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 }