github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/couchdb_test.go (about)

     1  package couchdb
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"reflect"
     8  	"strings"
     9  	"sync"
    10  	"testing"
    11  	"time"
    12  	"unsafe"
    13  
    14  	"github.com/cozy/cozy-stack/pkg/config/config"
    15  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    16  	"github.com/cozy/cozy-stack/pkg/prefixer"
    17  	"github.com/cozy/cozy-stack/pkg/realtime"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  const TestDoctype = "io.cozy.testobject"
    23  
    24  var TestPrefix = prefixer.NewPrefixer(0, "test", "couchdb-tests")
    25  var receivedEventsMutex sync.Mutex
    26  var receivedEvents map[string]*realtime.Event
    27  
    28  type testDoc struct {
    29  	TestID  string `json:"_id,omitempty"`
    30  	TestRev string `json:"_rev,omitempty"`
    31  	Test    string `json:"test"`
    32  	FieldA  string `json:"fieldA,omitempty"`
    33  	FieldB  int    `json:"fieldB,omitempty"`
    34  }
    35  
    36  func TestCouchdb(t *testing.T) {
    37  	if testing.Short() {
    38  		t.Skip("a couchdb is required for this test: test skipped due to the use of --short flag")
    39  	}
    40  
    41  	config.UseTestFile(t)
    42  
    43  	if _, err := CheckStatus(context.Background()); err != nil {
    44  		require.NoError(t, err, "This test need couchdb to run.")
    45  	}
    46  
    47  	require.NoError(t, ResetDB(TestPrefix, TestDoctype))
    48  
    49  	receivedEvents = make(map[string]*realtime.Event)
    50  	eventChan := realtime.GetHub().Subscriber(TestPrefix)
    51  	eventChan.Subscribe(TestDoctype)
    52  	go func() {
    53  		for ev := range eventChan.Channel {
    54  			receivedEventsMutex.Lock()
    55  			receivedEvents[ev.Verb+ev.Doc.ID()] = ev
    56  			receivedEventsMutex.Unlock()
    57  		}
    58  	}()
    59  
    60  	t.Cleanup(eventChan.Close)
    61  	t.Cleanup(func() { _ = DeleteDB(TestPrefix, TestDoctype) })
    62  
    63  	t.Run("Errors", func(t *testing.T) {
    64  		err := Error{StatusCode: 404, Name: "not_found", Reason: "missing"}
    65  		assert.Contains(t, err.Error(), "not_found")
    66  		assert.Contains(t, err.Error(), "missing")
    67  	})
    68  
    69  	t.Run("CreateDoc", func(t *testing.T) {
    70  		var err error
    71  
    72  		doc := makeTestDoc()
    73  		assert.Empty(t, doc.Rev(), doc.ID())
    74  
    75  		// Create the document
    76  		err = CreateDoc(TestPrefix, doc)
    77  		assert.NoError(t, err)
    78  		assert.NotEmpty(t, doc.Rev(), doc.ID())
    79  
    80  		docType, id := doc.DocType(), doc.ID()
    81  		assertGotEvent(t, realtime.EventCreate, doc.ID())
    82  
    83  		// Fetch it and see if its match
    84  		fetched := &testDoc{}
    85  		err = GetDoc(TestPrefix, docType, id, fetched)
    86  		assert.NoError(t, err)
    87  		assert.Equal(t, doc.ID(), fetched.ID())
    88  		assert.Equal(t, doc.Rev(), fetched.Rev())
    89  		assert.Equal(t, "somevalue", fetched.Test)
    90  
    91  		revBackup := fetched.Rev()
    92  
    93  		// Update it
    94  		updated := fetched
    95  		updated.Test = "changedvalue"
    96  		err = UpdateDoc(TestPrefix, updated)
    97  		assert.NoError(t, err)
    98  		assert.NotEqual(t, revBackup, updated.Rev())
    99  		assert.Equal(t, "changedvalue", updated.Test)
   100  		evt := assertGotEvent(t, realtime.EventUpdate, doc.ID())
   101  		assert.NotNil(t, evt.OldDoc)
   102  		assert.Equal(t, "somevalue", evt.OldDoc.(*testDoc).Test)
   103  		assert.Equal(t, "changedvalue", evt.Doc.(*testDoc).Test)
   104  
   105  		// Refetch it and see if its match
   106  		fetched2 := &testDoc{}
   107  		err = GetDoc(TestPrefix, docType, id, fetched2)
   108  		assert.NoError(t, err)
   109  		assert.Equal(t, doc.ID(), fetched2.ID())
   110  		assert.Equal(t, updated.Rev(), fetched2.Rev())
   111  		assert.Equal(t, "changedvalue", fetched2.Test)
   112  
   113  		// Delete it
   114  		err = DeleteDoc(TestPrefix, updated)
   115  		assert.NoError(t, err)
   116  		assertGotEvent(t, realtime.EventDelete, doc.ID())
   117  
   118  		fetched3 := &testDoc{}
   119  		err = GetDoc(TestPrefix, docType, id, fetched3)
   120  		assert.Error(t, err)
   121  		coucherr, iscoucherr := err.(*Error)
   122  		if assert.True(t, iscoucherr) {
   123  			assert.Equal(t, coucherr.Reason, "deleted")
   124  		}
   125  	})
   126  
   127  	t.Run("GetAllDocs", func(t *testing.T) {
   128  		doc1 := &testDoc{Test: "all_1"}
   129  		doc2 := &testDoc{Test: "all_2"}
   130  		assert.NoError(t, CreateDoc(TestPrefix, doc1))
   131  		assert.NoError(t, CreateDoc(TestPrefix, doc2))
   132  
   133  		var results []*testDoc
   134  		err := GetAllDocs(TestPrefix, TestDoctype, &AllDocsRequest{Limit: 2}, &results)
   135  		if assert.NoError(t, err) {
   136  			assert.Len(t, results, 2)
   137  			assert.Equal(t, results[0].Test, "all_1")
   138  			assert.Equal(t, results[1].Test, "all_2")
   139  		}
   140  	})
   141  
   142  	t.Run("GetDocRevs", func(t *testing.T) {
   143  		doc := &testDoc{Test: "1"}
   144  		assert.NoError(t, CreateDoc(TestPrefix, doc))
   145  		rev1 := doc.TestRev
   146  		doc.Test = "2"
   147  		assert.NoError(t, UpdateDoc(TestPrefix, doc))
   148  		rev2 := doc.TestRev
   149  
   150  		res := &JSONDoc{}
   151  		err := GetDocWithRevs(TestPrefix, TestDoctype, doc.TestID, res)
   152  		revisions := res.M["_revisions"].(map[string]interface{})
   153  		ids := revisions["ids"].([]interface{})
   154  		assert.NoError(t, err)
   155  		assert.Len(t, ids, 2)
   156  		sliceRev1 := strings.SplitN(rev1, "-", 2)
   157  		assert.Len(t, sliceRev1, 2)
   158  		sliceRev2 := strings.SplitN(rev2, "-", 2)
   159  		assert.Len(t, sliceRev2, 2)
   160  
   161  		assert.Equal(t, ids[0].(string), sliceRev2[1])
   162  		assert.Equal(t, ids[1].(string), sliceRev1[1])
   163  	})
   164  
   165  	t.Run("BulkUpdateDocs", func(t *testing.T) {
   166  		doc1 := &testDoc{Test: "before_1"}
   167  		doc2 := &testDoc{Test: "before_2"}
   168  		assert.NoError(t, CreateDoc(TestPrefix, doc1))
   169  		assert.NoError(t, CreateDoc(TestPrefix, doc2))
   170  
   171  		var results []*testDoc
   172  		err := GetAllDocs(TestPrefix, TestDoctype, &AllDocsRequest{Limit: 2}, &results)
   173  		assert.NoError(t, err)
   174  		results[0].Test = "after_1"
   175  		results[1].Test = "after_2"
   176  
   177  		olddocs := make([]interface{}, len(results))
   178  		docs := make([]interface{}, len(results))
   179  		for i, doc := range results {
   180  			docs[i] = doc
   181  		}
   182  		err = BulkUpdateDocs(TestPrefix, results[0].DocType(), docs, olddocs)
   183  		assert.NoError(t, err)
   184  
   185  		err = GetAllDocs(TestPrefix, TestDoctype, &AllDocsRequest{Limit: 2}, &results)
   186  		if assert.NoError(t, err) {
   187  			assert.Len(t, results, 2)
   188  			assert.Equal(t, results[0].Test, "after_1")
   189  			assert.Equal(t, results[1].Test, "after_2")
   190  		}
   191  	})
   192  
   193  	t.Run("DefineIndex", func(t *testing.T) {
   194  		err := DefineIndex(TestPrefix, mango.MakeIndex(TestDoctype, "my-index", mango.IndexDef{Fields: []string{"fieldA", "fieldB"}}))
   195  		assert.NoError(t, err)
   196  
   197  		// if I try to define the same index several time
   198  		err2 := DefineIndex(TestPrefix, mango.MakeIndex(TestDoctype, "my-index", mango.IndexDef{Fields: []string{"fieldA", "fieldB"}}))
   199  		assert.NoError(t, err2)
   200  	})
   201  
   202  	t.Run("DefineIndexWithPartialFilter", func(t *testing.T) {
   203  		err := DefineIndex(TestPrefix, mango.MakeIndex(TestDoctype, "my-index-with-partial-filter", mango.IndexDef{Fields: []string{"fieldA"}, PartialFilter: mango.NotExists("fieldB")}))
   204  		assert.NoError(t, err)
   205  
   206  		// if I try to define the same index several time
   207  		err2 := DefineIndex(TestPrefix, mango.MakeIndex(TestDoctype, "my-index-with-partial-filter", mango.IndexDef{Fields: []string{"fieldA"}, PartialFilter: mango.NotExists("fieldB")}))
   208  		assert.NoError(t, err2)
   209  	})
   210  
   211  	t.Run("Query", func(t *testing.T) {
   212  		// create a few docs for testing
   213  		doc1 := testDoc{FieldA: "value1", FieldB: 100}
   214  		doc2 := testDoc{FieldA: "value2", FieldB: 1000}
   215  		doc3 := testDoc{FieldA: "value2", FieldB: 300}
   216  		doc4 := testDoc{FieldA: "value1", FieldB: 1500}
   217  		doc5 := testDoc{FieldA: "value1", FieldB: 150}
   218  		docs := []*testDoc{&doc1, &doc2, &doc3, &doc4, &doc5}
   219  		for _, doc := range docs {
   220  			err := CreateDoc(TestPrefix, doc)
   221  			if !assert.NoError(t, err) || doc.ID() == "" {
   222  				t.FailNow()
   223  				return
   224  			}
   225  		}
   226  
   227  		err := DefineIndex(TestPrefix, mango.MakeIndex(TestDoctype, "my-index", mango.IndexDef{Fields: []string{"fieldA", "fieldB"}}))
   228  		if !assert.NoError(t, err) {
   229  			t.FailNow()
   230  			return
   231  		}
   232  		var out []testDoc
   233  		req := &FindRequest{
   234  			UseIndex: "my-index",
   235  			Selector: mango.And(
   236  				mango.Equal("fieldA", "value2"),
   237  				mango.Exists("fieldB"),
   238  			),
   239  		}
   240  		err = FindDocs(TestPrefix, TestDoctype, req, &out)
   241  		if assert.NoError(t, err) {
   242  			assert.Len(t, out, 2, "should get 2 results")
   243  			// if fieldA are equaly, docs will be ordered by fieldB
   244  			assert.Equal(t, doc3.ID(), out[0].ID())
   245  			assert.Equal(t, "value2", out[0].FieldA)
   246  			assert.Equal(t, doc2.ID(), out[1].ID())
   247  			assert.Equal(t, "value2", out[1].FieldA)
   248  		}
   249  
   250  		var out2 []testDoc
   251  		req2 := &FindRequest{
   252  			UseIndex: "my-index",
   253  			Selector: mango.And(
   254  				mango.Equal("fieldA", "value1"),
   255  				mango.Between("fieldB", 10, 1000),
   256  			),
   257  		}
   258  		err = FindDocs(TestPrefix, TestDoctype, req2, &out2)
   259  		if assert.NoError(t, err) {
   260  			assert.Len(t, out, 2, "should get 2 results")
   261  			assert.Equal(t, doc1.ID(), out2[0].ID())
   262  			assert.Equal(t, doc5.ID(), out2[1].ID())
   263  		}
   264  	})
   265  
   266  	t.Run("ForeachDocs", func(t *testing.T) {
   267  		for i := 0; i < 5; i++ {
   268  			doc := &testDoc{Test: fmt.Sprintf("foreach_%d", i)}
   269  			require.NoError(t, CreateDoc(TestPrefix, doc))
   270  		}
   271  
   272  		var results []*testDoc
   273  		err := GetAllDocs(TestPrefix, TestDoctype, &AllDocsRequest{}, &results)
   274  		require.NoError(t, err)
   275  		var expected []string
   276  		for _, result := range results {
   277  			expected = append(expected, result.Test)
   278  		}
   279  
   280  		var keys []string
   281  		ForeachDocsWithCustomPagination(TestPrefix, TestDoctype, 2, func(id string, raw json.RawMessage) error {
   282  			var doc testDoc
   283  			err := json.Unmarshal(raw, &doc)
   284  			if err != nil {
   285  				return err
   286  			}
   287  			keys = append(keys, doc.Test)
   288  			return nil
   289  		})
   290  
   291  		assert.Equal(t, expected, keys)
   292  	})
   293  
   294  	t.Run("ChangesSuccess", func(t *testing.T) {
   295  		err := ResetDB(TestPrefix, TestDoctype)
   296  		assert.NoError(t, err)
   297  
   298  		request := &ChangesRequest{
   299  			DocType: TestDoctype,
   300  		}
   301  		response, err := GetChanges(TestPrefix, request)
   302  		seqnoAfterCreates := response.LastSeq
   303  		assert.NoError(t, err)
   304  		assert.Len(t, response.Results, 0)
   305  
   306  		doc1 := makeTestDoc()
   307  		doc2 := makeTestDoc()
   308  		doc3 := makeTestDoc()
   309  		assert.NoError(t, CreateDoc(TestPrefix, doc1))
   310  		assert.NoError(t, CreateDoc(TestPrefix, doc2))
   311  		assert.NoError(t, CreateDoc(TestPrefix, doc3))
   312  
   313  		request = &ChangesRequest{
   314  			DocType: TestDoctype,
   315  			Since:   seqnoAfterCreates,
   316  		}
   317  
   318  		response, err = GetChanges(TestPrefix, request)
   319  		assert.NoError(t, err)
   320  		assert.Len(t, response.Results, 3)
   321  
   322  		request = &ChangesRequest{
   323  			DocType: TestDoctype,
   324  			Since:   seqnoAfterCreates,
   325  			Limit:   2,
   326  		}
   327  
   328  		response, err = GetChanges(TestPrefix, request)
   329  		assert.NoError(t, err)
   330  		assert.Len(t, response.Results, 2)
   331  
   332  		seqnoAfterCreates = response.LastSeq
   333  
   334  		doc4 := makeTestDoc()
   335  		assert.NoError(t, CreateDoc(TestPrefix, doc4))
   336  
   337  		request = &ChangesRequest{
   338  			DocType: TestDoctype,
   339  			Since:   seqnoAfterCreates,
   340  		}
   341  		response, err = GetChanges(TestPrefix, request)
   342  		assert.NoError(t, err)
   343  		assert.Len(t, response.Results, 2)
   344  	})
   345  
   346  	t.Run("EnsureDBExist", func(t *testing.T) {
   347  		defer func() { _ = DeleteDB(TestPrefix, "io.cozy.tests.db1") }()
   348  		_, err := DBStatus(TestPrefix, "io.cozy.tests.db1")
   349  		assert.True(t, IsNoDatabaseError(err))
   350  		assert.NoError(t, EnsureDBExist(TestPrefix, "io.cozy.tests.db1"))
   351  		_, err = DBStatus(TestPrefix, "io.cozy.tests.db1")
   352  		assert.NoError(t, err)
   353  	})
   354  
   355  	t.Run("UpdateJSONDoc", func(t *testing.T) {
   356  		var err error
   357  
   358  		doc := &JSONDoc{
   359  			Type: TestDoctype,
   360  			M: map[string]interface{}{
   361  				"test": "1",
   362  			},
   363  		}
   364  		assert.Empty(t, doc.Rev(), doc.ID())
   365  
   366  		// Create the document
   367  		err = CreateDoc(TestPrefix, doc)
   368  		assert.NoError(t, err)
   369  		assert.NotEmpty(t, doc.Rev(), doc.ID())
   370  		assertGotEvent(t, realtime.EventCreate, doc.ID())
   371  
   372  		// Update it
   373  		updated := &JSONDoc{
   374  			Type: TestDoctype,
   375  			M: map[string]interface{}{
   376  				"_id":  doc.ID(),
   377  				"_rev": doc.Rev(),
   378  				"test": "2",
   379  			},
   380  		}
   381  		err = UpdateDoc(TestPrefix, updated)
   382  		assert.NoError(t, err)
   383  		assert.NotEqual(t, doc.Rev(), updated.Rev())
   384  		assert.Equal(t, "2", updated.M["test"])
   385  		evt := assertGotEvent(t, realtime.EventUpdate, doc.ID())
   386  		assert.NotNil(t, evt)
   387  		assert.NotNil(t, evt.OldDoc)
   388  		assert.Equal(t, "1", evt.OldDoc.(*JSONDoc).M["test"])
   389  		assert.Equal(t, "2", evt.Doc.(*JSONDoc).M["test"])
   390  
   391  		// Remove the test field
   392  		noTest := &JSONDoc{
   393  			Type: TestDoctype,
   394  			M: map[string]interface{}{
   395  				"_id":  updated.ID(),
   396  				"_rev": updated.Rev(),
   397  				"foo":  "bar",
   398  			},
   399  		}
   400  		err = UpdateDoc(TestPrefix, noTest)
   401  		assert.NoError(t, err)
   402  		assert.NotEqual(t, updated.Rev(), noTest.Rev())
   403  		assert.Empty(t, noTest.M["test"])
   404  		evt = assertGotEvent(t, realtime.EventUpdate, doc.ID())
   405  		assert.NotNil(t, evt)
   406  		assert.NotNil(t, evt.OldDoc)
   407  		assert.Equal(t, "2", evt.OldDoc.(*JSONDoc).M["test"])
   408  		assert.Empty(t, evt.Doc.(*JSONDoc).M["test"])
   409  
   410  		// Add the test field
   411  		withTest := &JSONDoc{
   412  			Type: TestDoctype,
   413  			M: map[string]interface{}{
   414  				"_id":  noTest.ID(),
   415  				"_rev": noTest.Rev(),
   416  				"foo":  "baz",
   417  				"test": "3",
   418  			},
   419  		}
   420  		err = UpdateDoc(TestPrefix, withTest)
   421  		assert.NoError(t, err)
   422  		assert.NotEqual(t, noTest.Rev(), withTest.Rev())
   423  		assert.Equal(t, "3", withTest.M["test"])
   424  		evt = assertGotEvent(t, realtime.EventUpdate, doc.ID())
   425  		assert.NotNil(t, evt)
   426  		assert.NotNil(t, evt.OldDoc)
   427  		assert.Empty(t, evt.OldDoc.(*JSONDoc).M["test"])
   428  		assert.Equal(t, "3", evt.Doc.(*JSONDoc).M["test"])
   429  	})
   430  
   431  	t.Run("JSONDocClone", func(t *testing.T) {
   432  		var m map[string]interface{}
   433  		data := []byte(`{
   434  	"foo1": "bar",
   435  	"foo2": [0,1,2,3],
   436  	"foo3": ["abc", 1, 1.1],
   437  	"foo4": {
   438  		"bar1":"bar",
   439  		"bar2": [0,1,2,3],
   440  		"bar3": ["abc", 1, 1.1, { "key": "value", "key2": [{}, 1, 2, 3] }],
   441  		"bar4": {}
   442  	},
   443  	"foo5": 1,
   444  	"foo6": 0.001,
   445  	"foo7": "toto"
   446  }`)
   447  
   448  		err := json.Unmarshal(data, &m)
   449  		assert.NoError(t, err)
   450  		j1 := JSONDoc{
   451  			Type: "toto",
   452  			M:    m,
   453  		}
   454  		j2 := j1.Clone().(*JSONDoc)
   455  
   456  		assert.Equal(t, j1.Type, j2.Type)
   457  		assert.True(t, reflect.DeepEqual(j1.M, j2.M))
   458  
   459  		assert.False(t, reflect.ValueOf(j1.M["foo2"]).Pointer() == reflect.ValueOf(j2.M["foo2"]).Pointer())
   460  		assert.False(t, reflect.ValueOf(j1.M["foo3"]).Pointer() == reflect.ValueOf(j2.M["foo3"]).Pointer())
   461  		assert.False(t, reflect.ValueOf(j1.M["foo4"]).Pointer() == reflect.ValueOf(j2.M["foo4"]).Pointer())
   462  
   463  		s1 := j1.M["foo1"].(string)
   464  		s2 := j2.M["foo1"].(string)
   465  		s3 := j1.M["foo7"].(string)
   466  		s4 := j2.M["foo7"].(string)
   467  
   468  		hdr1 := (*reflect.StringHeader)(unsafe.Pointer(&s1))
   469  		hdr2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))
   470  		hdr3 := (*reflect.StringHeader)(unsafe.Pointer(&s3))
   471  		hdr4 := (*reflect.StringHeader)(unsafe.Pointer(&s4))
   472  
   473  		assert.Equal(t, hdr1.Data, hdr2.Data)
   474  		assert.Equal(t, hdr1.Len, hdr2.Len)
   475  
   476  		assert.Equal(t, hdr3.Data, hdr4.Data)
   477  		assert.Equal(t, hdr3.Len, hdr4.Len)
   478  
   479  		assert.NotEqual(t, hdr1.Data, hdr4.Data)
   480  		assert.NotEqual(t, hdr1.Len, hdr4.Len)
   481  	})
   482  
   483  	t.Run("LocalDocuments", func(t *testing.T) {
   484  		id := "foo"
   485  		_, err := GetLocal(TestPrefix, TestDoctype, id)
   486  		assert.True(t, IsNotFoundError(err))
   487  
   488  		doc := map[string]interface{}{"bar": "baz"}
   489  		err = PutLocal(TestPrefix, TestDoctype, id, doc)
   490  		assert.NoError(t, err)
   491  		assert.NotEmpty(t, doc["_rev"])
   492  
   493  		out, err := GetLocal(TestPrefix, TestDoctype, id)
   494  		assert.NoError(t, err)
   495  		assert.Equal(t, "baz", out["bar"])
   496  
   497  		err = DeleteLocal(TestPrefix, TestDoctype, id)
   498  		assert.NoError(t, err)
   499  
   500  		_, err = GetLocal(TestPrefix, TestDoctype, id)
   501  		assert.True(t, IsNotFoundError(err))
   502  	})
   503  }
   504  
   505  func (t *testDoc) ID() string {
   506  	return t.TestID
   507  }
   508  
   509  func (t *testDoc) Rev() string {
   510  	return t.TestRev
   511  }
   512  
   513  func (t *testDoc) DocType() string {
   514  	return TestDoctype
   515  }
   516  
   517  func (t *testDoc) Clone() Doc {
   518  	cloned := *t
   519  	return &cloned
   520  }
   521  
   522  func (t *testDoc) SetID(id string) {
   523  	t.TestID = id
   524  }
   525  
   526  func (t *testDoc) SetRev(rev string) {
   527  	t.TestRev = rev
   528  }
   529  
   530  func makeTestDoc() Doc {
   531  	return &testDoc{
   532  		Test: "somevalue",
   533  	}
   534  }
   535  
   536  func assertGotEvent(t *testing.T, eventType, id string) *realtime.Event {
   537  	t.Helper()
   538  
   539  	var event *realtime.Event
   540  	var ok bool
   541  
   542  	for i := 0; i < 200; i++ {
   543  		receivedEventsMutex.Lock()
   544  		event, ok = receivedEvents[eventType+id]
   545  		receivedEventsMutex.Unlock()
   546  		if ok {
   547  			delete(receivedEvents, eventType+id)
   548  			break
   549  		}
   550  		time.Sleep(time.Millisecond)
   551  	}
   552  
   553  	assert.True(t, ok, "Expected event %s:%s", eventType, id)
   554  	return event
   555  }