github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/pkg/couchdb/couchdb_test.go (about)

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