diff --git a/base/bucket_gocb_test.go b/base/bucket_gocb_test.go index 0ca23adc8e..24bfebc679 100644 --- a/base/bucket_gocb_test.go +++ b/base/bucket_gocb_test.go @@ -18,6 +18,7 @@ import ( "os" "strconv" "strings" + "sync" "testing" "time" @@ -25,6 +26,7 @@ import ( sgbucket "github.com/couchbase/sg-bucket" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) func TestSetGet(t *testing.T) { @@ -756,10 +758,6 @@ func TestXattrWriteCasRaw(t *testing.T) { // TestWriteCasTombstoneResurrect. Verifies writing a new document body and xattr to a logically deleted document (xattr still exists) func TestXattrWriteCasTombstoneResurrect(t *testing.T) { - if UnitTestUrlIsWalrus() { - t.Skip("Test requires Couchbase Server bucket when using xattrs") - } - ctx := TestCtx(t) bucket := GetTestBucket(t) defer bucket.Close(ctx) @@ -1573,11 +1571,6 @@ func TestXattrRetrieveDocumentAndXattr(t *testing.T) { // TestXattrMutateDocAndXattr. Validates mutation of doc + xattr in various possible previous states of the document. func TestXattrMutateDocAndXattr(t *testing.T) { - // Skipping on non-Couchbase until CBG-3392 is fixed - if UnitTestUrlIsWalrus() { - t.Skip("Test requires Couchbase Server bucket when using xattrs") - } - ctx := TestCtx(t) bucket := GetTestBucket(t) defer bucket.Close(ctx) @@ -3637,3 +3630,1254 @@ func TestRemoveXattrsAndDeleteSubDocPathsErrors(t *testing.T) { require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) }) } + +// TestDataStoreOperationErrors checks the result of each write, delete and remove operation against a missing key, +// a tombstone and a live document, with a CAS of 0 (no CAS check), the current CAS and a stale CAS. +func TestDataStoreOperationErrors(t *testing.T) { + const ( + success = "success" + notFound = "not found" + casMismatch = "cas mismatch" + xattrName = "_testxattr" + ) + type docState string + const ( + missing docState = "missing" + tombstone docState = "tombstone" + live docState = "live" + ) + type casArg string + const ( + zeroCas casArg = "zeroCas" + currentCas casArg = "currentCas" + staleCas casArg = "staleCas" + ) + + ctx := TestCtx(t) + bucket := GetTestBucket(t) + defer bucket.Close(ctx) + dataStore := bucket.GetSingleDataStore() + + createDoc := func(t *testing.T, state docState) (key string, cas uint64) { + key = t.Name() + if state == missing { + return key, 0 + } + cas, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), map[string][]byte{xattrName: []byte(`{"seq":1}`)}, nil, nil) + require.NoError(t, err) + if state == tombstone { + cas, err = dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + } + return key, cas + } + + testCases := []struct { + name string + op func(key string, cas uint64) error + expected map[docState]map[casArg]string // a single zeroCas entry for operations that take no CAS + verify func(t *testing.T, key string, state docState) // optional check of the document after a success + }{ + { + name: "Delete", + op: func(key string, _ uint64) error { return dataStore.Delete(ctx, key) }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: notFound}, + tombstone: {zeroCas: notFound}, + live: {zeroCas: success}, + }, + }, + { + name: "Remove", + op: func(key string, cas uint64) error { + _, err := dataStore.Remove(ctx, key, cas) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: notFound}, + tombstone: {zeroCas: notFound, currentCas: notFound, staleCas: notFound}, + live: {zeroCas: success, currentCas: success, staleCas: casMismatch}, + }, + }, + { + name: "DeleteWithXattrs", + op: func(key string, _ uint64) error { return dataStore.DeleteWithXattrs(ctx, key, []string{xattrName}) }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: notFound}, + tombstone: {zeroCas: success}, + live: {zeroCas: success}, + }, + }, + { + name: "WriteTombstoneWithXattrs", + op: func(key string, cas uint64) error { + _, err := dataStore.WriteTombstoneWithXattrs(ctx, key, 0, cas, map[string][]byte{xattrName: []byte(`{"seq":2}`)}, nil, true, nil) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: notFound}, + tombstone: {zeroCas: notFound, currentCas: notFound, staleCas: notFound}, + live: {zeroCas: success, currentCas: success, staleCas: casMismatch}, + }, + }, + { + name: "RemoveXattrs", + op: func(key string, cas uint64) error { return dataStore.RemoveXattrs(ctx, key, []string{xattrName}, cas) }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: notFound}, + tombstone: {zeroCas: success, currentCas: success, staleCas: casMismatch}, + live: {zeroCas: success, currentCas: success, staleCas: casMismatch}, + }, + }, + { + name: "DeleteSubDocPaths", + op: func(key string, _ uint64) error { return dataStore.DeleteSubDocPaths(ctx, key, xattrName) }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: notFound}, + tombstone: {zeroCas: notFound}, + live: {zeroCas: success}, + }, + }, + { + name: "WriteCas", + op: func(key string, cas uint64) error { + _, err := dataStore.WriteCas(ctx, key, 0, cas, map[string]any{"foo": "baz"}, 0) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: success}, + tombstone: {zeroCas: success, currentCas: notFound, staleCas: notFound}, + live: {zeroCas: casMismatch, currentCas: success, staleCas: casMismatch}, + }, + }, + { + name: "WriteSubDoc", + op: func(key string, cas uint64) error { + _, err := dataStore.WriteSubDoc(ctx, key, "foo", cas, []byte(`"baz"`)) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: success}, + tombstone: {zeroCas: success, currentCas: success, staleCas: success}, + live: {zeroCas: success, currentCas: success, staleCas: casMismatch}, + }, + }, + { + name: "WriteWithXattrs", + op: func(key string, cas uint64) error { + _, err := dataStore.WriteWithXattrs(ctx, key, 0, cas, []byte(`{"foo":"baz"}`), map[string][]byte{xattrName: []byte(`{"seq":2}`)}, nil, nil) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: success}, + tombstone: {zeroCas: success, currentCas: notFound, staleCas: notFound}, + live: {zeroCas: casMismatch, currentCas: success, staleCas: casMismatch}, + }, + }, + { + name: "WriteResurrectionWithXattrs", + op: func(key string, _ uint64) error { + _, err := dataStore.WriteResurrectionWithXattrs(ctx, key, 0, []byte(`{"foo":"baz"}`), map[string][]byte{xattrName: []byte(`{"seq":2}`)}, nil) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: success}, + tombstone: {zeroCas: success}, + live: {zeroCas: casMismatch}, + }, + }, + { + name: "SetXattrs", + op: func(key string, _ uint64) error { + _, err := dataStore.SetXattrs(ctx, key, map[string][]byte{xattrName: []byte(`{"seq":2}`)}) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: success}, + tombstone: {zeroCas: success}, + live: {zeroCas: success}, + }, + verify: func(t *testing.T, key string, state docState) { + body, _, err := dataStore.GetRaw(ctx, key) + switch state { + case missing: + require.NoError(t, err) + require.JSONEq(t, `{}`, string(body)) + case tombstone: + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + case live: + require.NoError(t, err) + require.JSONEq(t, `{"foo":"bar"}`, string(body)) + } + }, + }, + { + name: "UpdateXattrs", + op: func(key string, cas uint64) error { + _, err := dataStore.UpdateXattrs(ctx, key, 0, cas, map[string][]byte{xattrName: []byte(`{"seq":2}`)}, nil) + return err + }, + expected: map[docState]map[casArg]string{ + missing: {zeroCas: notFound}, + tombstone: {zeroCas: success, currentCas: success, staleCas: casMismatch}, + live: {zeroCas: success, currentCas: success, staleCas: casMismatch}, + }, + }, + } + for _, tc := range testCases { + for _, state := range []docState{missing, tombstone, live} { + for _, arg := range []casArg{zeroCas, currentCas, staleCas} { + expected, ok := tc.expected[state][arg] + if !ok { + continue + } + t.Run(fmt.Sprintf("%s/%s/%s", tc.name, state, arg), func(t *testing.T) { + key, cas := createDoc(t, state) + switch arg { + case zeroCas: + cas = 0 + case staleCas: + cas-- + } + err := tc.op(key, cas) + switch expected { + case success: + require.NoError(t, err) + case notFound: + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + case casMismatch: + require.True(t, IsCasMismatch(err), "expected cas mismatch error, got %v", err) + } + if expected == success && tc.verify != nil { + tc.verify(t, key, state) + } + }) + } + } + } +} + +// TestDeleteAfterTombstoneResurrection checks that a document written over a tombstone is live again, and that a +// document removed by DeleteWithXattrs is a tombstone, by deleting it afterwards. +func TestDeleteAfterTombstoneResurrection(t *testing.T) { + ctx := TestCtx(t) + bucket := GetTestBucket(t) + defer bucket.Close(ctx) + dataStore := bucket.GetSingleDataStore() + + body := map[string]any{"foo": "bar"} + rawBody := []byte(`{"foo":"bar"}`) + writes := map[string]func(key string) error{ + "Set": func(key string) error { return dataStore.Set(ctx, key, 0, nil, body) }, + "SetRaw": func(key string) error { return dataStore.SetRaw(ctx, key, 0, nil, rawBody) }, + "Incr": func(key string) error { + _, err := dataStore.Incr(ctx, key, 1, 1, 0) + return err + }, + "Add": func(key string) error { + added, err := dataStore.Add(ctx, key, 0, body) + require.True(t, added) + return err + }, + "AddRaw": func(key string) error { + added, err := dataStore.AddRaw(ctx, key, 0, rawBody) + require.True(t, added) + return err + }, + "WriteCas": func(key string) error { + _, err := dataStore.WriteCas(ctx, key, 0, 0, body, 0) + return err + }, + "Update": func(key string) error { + _, err := dataStore.Update(ctx, key, 0, func([]byte) ([]byte, *uint32, bool, error) { + return rawBody, nil, false, nil + }) + return err + }, + } + for name, write := range writes { + t.Run(name, func(t *testing.T) { + key := t.Name() + require.NoError(t, dataStore.SetRaw(ctx, key, 0, nil, rawBody)) + require.NoError(t, dataStore.Delete(ctx, key)) + + require.NoError(t, write(key)) + require.NoError(t, dataStore.Delete(ctx, key)) + }) + } + + t.Run("DeleteWithXattrs", func(t *testing.T) { + key := t.Name() + _, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, rawBody, map[string][]byte{"_testxattr": []byte(`{"seq":1}`)}, nil, nil) + require.NoError(t, err) + require.NoError(t, dataStore.DeleteWithXattrs(ctx, key, []string{"_testxattr"})) + err = dataStore.Delete(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + }) +} + +// TestDeleteWithXattrsMissingXattr checks the result and the remaining document when DeleteWithXattrs is asked to +// delete an xattr that the document does not have. +func TestDeleteWithXattrsMissingXattr(t *testing.T) { + const ( + success = "success" + notFound = "not found" + otherError = "other error" + missingName = "_missingxattr" + ) + ctx := TestCtx(t) + bucket := GetTestBucket(t) + defer bucket.Close(ctx) + dataStore := bucket.GetSingleDataStore() + + testCases := []struct { + name string + tombstone bool + xattrs []string + deleteXattrs []string + expected string + expectedRemaining []string + }{ + { + name: "live doc, delete present and missing xattr", + xattrs: []string{"_xattr1"}, + deleteXattrs: []string{"_xattr1", missingName}, + expected: success, + expectedRemaining: []string{"_xattr1"}, + }, + { + name: "live doc, delete one of two xattrs and a missing xattr", + xattrs: []string{"_xattr1", "_xattr2"}, + deleteXattrs: []string{"_xattr1", missingName}, + expected: success, + expectedRemaining: []string{"_xattr1", "_xattr2"}, + }, + { + name: "live doc, delete missing xattr", + xattrs: []string{"_xattr1"}, + deleteXattrs: []string{missingName}, + expected: success, + expectedRemaining: []string{"_xattr1"}, + }, + { + name: "live doc with no xattrs, delete missing xattr", + deleteXattrs: []string{missingName}, + expected: success, + }, + { + name: "tombstone, delete present and missing xattr", + tombstone: true, + xattrs: []string{"_xattr1"}, + deleteXattrs: []string{"_xattr1", missingName}, + expected: otherError, + expectedRemaining: []string{"_xattr1"}, + }, + { + name: "tombstone, delete missing xattr", + tombstone: true, + xattrs: []string{"_xattr1"}, + deleteXattrs: []string{missingName}, + expected: notFound, + expectedRemaining: []string{"_xattr1"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + key := t.Name() + xattrs := make(map[string][]byte, len(tc.xattrs)) + for _, xattrName := range tc.xattrs { + xattrs[xattrName] = []byte(`{"seq":1}`) + } + var cas uint64 + var err error + if len(xattrs) > 0 { + cas, err = dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), xattrs, nil, nil) + } else { + cas, err = dataStore.WriteCas(ctx, key, 0, 0, map[string]any{"foo": "bar"}, 0) + } + require.NoError(t, err) + if tc.tombstone { + _, err = dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + } + + err = dataStore.DeleteWithXattrs(ctx, key, tc.deleteXattrs) + switch tc.expected { + case success: + require.NoError(t, err) + case notFound: + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + case otherError: + require.Error(t, err) + require.False(t, IsDocNotFoundError(err), "expected an error other than not found, got %v", err) + } + + _, _, err = dataStore.GetRaw(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected no document body, got %v", err) + var remaining []string + for _, xattrName := range append(tc.xattrs, missingName) { + xattrValues, _, err := dataStore.GetXattrs(ctx, key, []string{xattrName}) + if err == nil && len(xattrValues[xattrName]) > 0 { + remaining = append(remaining, xattrName) + } + } + require.Equal(t, tc.expectedRemaining, remaining) + }) + } +} + +// TestXattrOnlyWriteKeepsTombstone checks that writing only xattrs to a tombstone keeps it a tombstone, and that the +// feed reports the write as a deletion. +func TestXattrOnlyWriteKeepsTombstone(t *testing.T) { + ctx := TestCtx(t) + bucket := GetTestBucket(t) + defer bucket.Close(ctx) + dataStore := bucket.GetSingleDataStore() + + type feedEvent struct { + opcode sgbucket.FeedOpcode + cas uint64 + } + var eventsLock sync.Mutex + events := make(map[string][]feedEvent) + dcpClient, err := NewDCPClient(ctx, bucket, DCPClientOptions{ + FeedID: t.Name(), + Callback: func(event sgbucket.FeedEvent) bool { + eventsLock.Lock() + defer eventsLock.Unlock() + events[string(event.Key)] = append(events[string(event.Key)], feedEvent{opcode: event.Opcode, cas: event.Cas}) + return true + }, + CheckpointPrefix: DefaultMetadataKeys.DCPCheckpointPrefix(t.Name()), + CollectionNames: NewCollectionNameSet(dataStore), + FromLatestSequence: true, + MetadataStore: bucket.GetMetadataStore(), + }) + require.NoError(t, err) + doneChan, err := dcpClient.Start() + require.NoError(t, err) + defer func() { + assert.NoError(t, dcpClient.Close()) + RequireChanClosed(t, doneChan) + }() + + writes := map[string]func(key string, cas uint64) error{ + "WriteTombstoneWithXattrs": func(key string, cas uint64) error { + _, err := dataStore.WriteTombstoneWithXattrs(ctx, key, 0, cas, map[string][]byte{SyncXattrName: []byte(`{"seq":2}`)}, nil, false, nil) + return err + }, + "UpdateXattrs": func(key string, cas uint64) error { + _, err := dataStore.UpdateXattrs(ctx, key, 0, cas, map[string][]byte{SyncXattrName: []byte(`{"seq":2}`)}, nil) + return err + }, + "SetXattrs": func(key string, _ uint64) error { + _, err := dataStore.SetXattrs(ctx, key, map[string][]byte{SyncXattrName: []byte(`{"seq":2}`)}) + return err + }, + "RemoveXattrs": func(key string, cas uint64) error { + return dataStore.RemoveXattrs(ctx, key, []string{SyncXattrName}, cas) + }, + } + for name, write := range writes { + t.Run(name, func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + cas, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"body":1}`), map[string][]byte{SyncXattrName: []byte(`{"seq":1}`)}, nil, nil) + require.NoError(t, err) + cas, err = dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + + require.NoError(t, write(key, cas)) + _, writeCas, err := dataStore.GetXattrs(ctx, key, []string{VirtualXattrRevSeqNo}) + require.NoError(t, err) + require.EventuallyWithT(t, func(c *assert.CollectT) { + eventsLock.Lock() + defer eventsLock.Unlock() + assert.Contains(c, events[key], feedEvent{opcode: sgbucket.FeedOpDeletion, cas: writeCas}) + }, 10*time.Second, 10*time.Millisecond) + _, _, err = dataStore.GetRaw(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + err = dataStore.Delete(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + }) + } +} + +// requireRevSeqNo returns the revision sequence number of a document or tombstone. +func requireRevSeqNo(t *testing.T, dataStore sgbucket.DataStore, key string) uint64 { + xattrs, _, err := dataStore.GetXattrs(TestCtx(t), key, []string{VirtualXattrRevSeqNo}) + require.NoError(t, err) + var revSeqNo string + require.NoError(t, JSONUnmarshal(xattrs[VirtualXattrRevSeqNo], &revSeqNo)) + n, err := strconv.ParseUint(revSeqNo, 10, 64) + require.NoError(t, err) + return n +} + +// testTombstoneRemovesUserXattrs checks that each way of deleting a doc keeps system xattrs and removes user xattrs. +func testTombstoneRemovesUserXattrs(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + + deletes := map[string]func(t *testing.T, key string, cas uint64){ + "Delete": func(t *testing.T, key string, _ uint64) { require.NoError(t, dataStore.Delete(ctx, key)) }, + "Remove": func(t *testing.T, key string, cas uint64) { + _, err := dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + }, + "UpdateDelete": func(t *testing.T, key string, _ uint64) { + _, err := dataStore.Update(ctx, key, 0, func([]byte) ([]byte, *uint32, bool, error) { return nil, nil, true, nil }) + require.NoError(t, err) + }, + } + for name, deleteFn := range deletes { + t.Run(name, func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + cas, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), + map[string][]byte{SyncXattrName: []byte(`{"rev":"1-a"}`), "user": []byte(`{"a":1}`)}, nil, nil) + require.NoError(t, err) + + deleteFn(t, key, cas) + + xattrs, _, err := dataStore.GetXattrs(ctx, key, []string{SyncXattrName}) + require.NoError(t, err) + require.JSONEq(t, `{"rev":"1-a"}`, string(xattrs[SyncXattrName])) + _, _, err = dataStore.GetXattrs(ctx, key, []string{"user"}) + require.True(t, IsXattrNotFoundError(err), "expected xattr not found error, got %v", err) + }) + } +} + +// testWriteCasNil checks that a nil WriteCas value never creates a tombstone: an untyped nil writes a null JSON body +// and a nil []byte writes an empty body. +func testWriteCasNil(t *testing.T, dataStore sgbucket.DataStore) { + type docState string + const ( + missing docState = "missing" + tombstone docState = "tombstone" + live docState = "live" + ) + ctx := TestCtx(t) + + testCases := []struct { + name string + state docState + value any + opt sgbucket.WriteOptions + expectedBody string + }{ + {name: "untypedNil", state: missing, expectedBody: "null"}, + {name: "untypedNil", state: tombstone, expectedBody: "null"}, + {name: "untypedNil", state: live, expectedBody: "null"}, + {name: "nilBytes", state: missing, value: []byte(nil)}, + {name: "nilBytes", state: live, value: []byte(nil)}, + {name: "nilBytesRaw", state: missing, value: []byte(nil), opt: sgbucket.Raw}, + {name: "nilBytesRaw", state: live, value: []byte(nil), opt: sgbucket.Raw}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s/%s", tc.name, tc.state), func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + var cas, revSeqNo uint64 + if tc.state != missing { + var err error + cas, err = dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), map[string][]byte{SyncXattrName: []byte(`{"rev":"1-a"}`)}, nil, nil) + require.NoError(t, err) + if tc.state == tombstone { + require.NoError(t, dataStore.Delete(ctx, key)) + cas = 0 + } + revSeqNo = requireRevSeqNo(t, dataStore, key) + } + _, err := dataStore.WriteCas(ctx, key, 0, cas, tc.value, tc.opt) + require.NoError(t, err) + + body, _, err := dataStore.GetRaw(ctx, key) + require.NoError(t, err) + require.Equal(t, tc.expectedBody, string(body)) + require.Equal(t, revSeqNo+1, requireRevSeqNo(t, dataStore, key)) + xattrs, _, err := dataStore.GetXattrs(ctx, key, []string{SyncXattrName}) + if tc.state == live { + require.NoError(t, err) + require.JSONEq(t, `{"rev":"1-a"}`, string(xattrs[SyncXattrName])) + } else { + require.True(t, IsXattrNotFoundError(err), "expected xattr not found error, got %v", err) + } + }) + } +} + +// testTombstoneFlagMatchesBody checks that after each write over a tombstone, the document is live if it has a body +// and a tombstone if it does not, and that the write increases the revision sequence number. +func testTombstoneFlagMatchesBody(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + + syncXattr := map[string][]byte{SyncXattrName: []byte(`{"rev":"2-a"}`)} + testCases := []struct { + name string + expectedLive bool + writeFn func(t *testing.T, key string, cas uint64) + }{ + { + name: "Add", + expectedLive: true, + writeFn: func(t *testing.T, key string, _ uint64) { + added, err := dataStore.Add(ctx, key, 0, map[string]any{"new": true}) + require.NoError(t, err) + require.True(t, added) + }, + }, + { + name: "Set", + expectedLive: true, + writeFn: func(t *testing.T, key string, _ uint64) { + require.NoError(t, dataStore.Set(ctx, key, 0, nil, map[string]any{"new": true})) + }, + }, + { + name: "AddRaw", + expectedLive: true, + writeFn: func(t *testing.T, key string, _ uint64) { + added, err := dataStore.AddRaw(ctx, key, 0, []byte(`{"new":true}`)) + require.NoError(t, err) + require.True(t, added) + }, + }, + { + name: "SetRaw", + expectedLive: true, + writeFn: func(t *testing.T, key string, _ uint64) { + require.NoError(t, dataStore.SetRaw(ctx, key, 0, nil, []byte(`{"new":true}`))) + }, + }, + { + name: "Incr", + expectedLive: true, + writeFn: func(t *testing.T, key string, _ uint64) { + _, err := dataStore.Incr(ctx, key, 1, 1, 0) + require.NoError(t, err) + }, + }, + { + name: "WriteCas", + expectedLive: true, + writeFn: func(t *testing.T, key string, _ uint64) { + _, err := dataStore.WriteCas(ctx, key, 0, 0, map[string]any{"new": true}, 0) + require.NoError(t, err) + }, + }, + { + name: "SetXattrs", + writeFn: func(t *testing.T, key string, _ uint64) { + _, err := dataStore.SetXattrs(ctx, key, syncXattr) + require.NoError(t, err) + }, + }, + { + name: "UpdateXattrs", + writeFn: func(t *testing.T, key string, cas uint64) { + _, err := dataStore.UpdateXattrs(ctx, key, 0, cas, syncXattr, nil) + require.NoError(t, err) + }, + }, + { + name: "WriteWithXattrsNilBody", + writeFn: func(t *testing.T, key string, cas uint64) { + _, err := dataStore.WriteWithXattrs(ctx, key, 0, cas, nil, syncXattr, nil, nil) + require.NoError(t, err) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + _, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), map[string][]byte{SyncXattrName: []byte(`{"rev":"1-a"}`)}, nil, nil) + require.NoError(t, err) + require.NoError(t, dataStore.Delete(ctx, key)) + _, cas, err := dataStore.GetXattrs(ctx, key, []string{SyncXattrName}) + require.NoError(t, err) + revSeqNo := requireRevSeqNo(t, dataStore, key) + + tc.writeFn(t, key, cas) + + require.Greater(t, requireRevSeqNo(t, dataStore, key), revSeqNo) + _, _, err = dataStore.GetRaw(ctx, key) + if tc.expectedLive { + require.NoError(t, err) + require.NoError(t, dataStore.Delete(ctx, key)) + } else { + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + err = dataStore.Delete(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + } + }) + } +} + +// testUpdateByDocState checks the document after an Update over a missing, tombstone or live document, when the +// callback returns a value or nil, with and without a delete. +func testUpdateByDocState(t *testing.T, dataStore sgbucket.DataStore) { + type docState string + const ( + missing docState = "missing" + tombstone docState = "tombstone" + live docState = "live" + ) + ctx := TestCtx(t) + + testCases := []struct { + state docState + value []byte + delete bool + expectedTombstone bool // otherwise a live document with expectedBody + expectedBody string // empty for an empty body + expectedExpiry bool + expectedSync bool + }{ + {state: missing, expectedExpiry: true}, + {state: tombstone, expectedExpiry: true}, + {state: live, expectedExpiry: true, expectedSync: true}, + {state: missing, delete: true, expectedExpiry: true}, + {state: tombstone, delete: true, expectedExpiry: true}, + {state: live, delete: true, expectedTombstone: true, expectedSync: true}, + {state: tombstone, value: []byte(`{"new":true}`), expectedBody: `{"new":true}`, expectedExpiry: true}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s/value=%t/delete=%t", tc.state, tc.value != nil, tc.delete), func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + if tc.state != missing { + cas, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), map[string][]byte{SyncXattrName: []byte(`{"seq":1}`)}, nil, nil) + require.NoError(t, err) + if tc.state == tombstone { + _, err = dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + _, cas, err = dataStore.GetRaw(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + require.Zero(t, cas, "the read of a tombstone returned a CAS") + } + } + + exp := uint32(3600) + _, err := dataStore.Update(ctx, key, 0, func([]byte) ([]byte, *uint32, bool, error) { + return tc.value, &exp, tc.delete, nil + }) + require.NoError(t, err) + + body, _, err := dataStore.GetRaw(ctx, key) + switch { + case tc.expectedTombstone: + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + case tc.expectedBody == "": + require.NoError(t, err) + require.Empty(t, body) + default: + require.NoError(t, err) + require.JSONEq(t, tc.expectedBody, string(body)) + } + docExp, err := dataStore.GetExpiry(ctx, key) + require.NoError(t, err) + require.Equal(t, tc.expectedExpiry, docExp != 0, "unexpected expiry %d", docExp) + xattrs, _, err := dataStore.GetXattrs(ctx, key, []string{SyncXattrName}) + if tc.expectedSync { + require.NoError(t, err) + require.JSONEq(t, `{"seq":1}`, string(xattrs[SyncXattrName])) + } else { + require.True(t, IsXattrNotFoundError(err), "expected xattr not found error, got %v", err) + } + }) + } +} + +// testTombstoneExpiry checks the expiry of the tombstone that each operation creates, when the document and the +// operation have expiries far in the future, and that a write which resurrects it with PreserveExpiry has no expiry. +func testTombstoneExpiry(t *testing.T, dataStore sgbucket.DataStore) { + type docState string + const ( + missing docState = "missing" + tombstone docState = "tombstone" + live docState = "live" + ) + ctx := TestCtx(t) + + docExpiry := uint32(time.Now().Add(5 * 365 * 24 * time.Hour).Unix()) + opExpiry := uint32(time.Now().Add(10 * 365 * 24 * time.Hour).Unix()) + syncXattr := func(seq int) map[string][]byte { + return map[string][]byte{SyncXattrName: fmt.Appendf(nil, `{"seq":%d}`, seq)} + } + writeUpdateTombstone := func(t *testing.T, key string, exp uint32, callbackExp *uint32, opts *sgbucket.MutateInOptions) { + _, err := dataStore.WriteUpdateWithXattrs(ctx, key, []string{SyncXattrName}, exp, nil, opts, func([]byte, map[string][]byte, uint64) (sgbucket.UpdatedDoc, error) { + return sgbucket.UpdatedDoc{Xattrs: syncXattr(2), IsTombstone: true, Expiry: callbackExp}, nil + }) + require.NoError(t, err) + } + + testCases := []struct { + name string + state docState + takesExp bool // the operation takes an expiry + takesOpts bool // the operation takes MutateInOptions + keepsOpExp bool // the tombstone has the expiry passed to the operation + op func(t *testing.T, key string, cas uint64, exp uint32, opts *sgbucket.MutateInOptions) + }{ + { + name: "Delete", + state: live, + op: func(t *testing.T, key string, _ uint64, _ uint32, _ *sgbucket.MutateInOptions) { + require.NoError(t, dataStore.Delete(ctx, key)) + }, + }, + { + name: "Remove", + state: live, + op: func(t *testing.T, key string, cas uint64, _ uint32, _ *sgbucket.MutateInOptions) { + _, err := dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + }, + }, + { + name: "DeleteWithXattrs", + state: live, + op: func(t *testing.T, key string, _ uint64, _ uint32, _ *sgbucket.MutateInOptions) { + require.NoError(t, dataStore.DeleteWithXattrs(ctx, key, nil)) + }, + }, + { + name: "UpdateDelete", + state: live, + takesExp: true, + op: func(t *testing.T, key string, _ uint64, exp uint32, _ *sgbucket.MutateInOptions) { + _, err := dataStore.Update(ctx, key, 0, func([]byte) ([]byte, *uint32, bool, error) { return nil, &exp, true, nil }) + require.NoError(t, err) + }, + }, + { + name: "WriteTombstoneWithXattrsDeleteBody", + state: live, + takesExp: true, + takesOpts: true, + keepsOpExp: true, + op: func(t *testing.T, key string, cas uint64, exp uint32, opts *sgbucket.MutateInOptions) { + _, err := dataStore.WriteTombstoneWithXattrs(ctx, key, exp, cas, syncXattr(2), nil, true, opts) + require.NoError(t, err) + }, + }, + { + name: "WriteTombstoneWithXattrsMissing", + state: missing, + takesExp: true, + takesOpts: true, + keepsOpExp: true, + op: func(t *testing.T, key string, _ uint64, exp uint32, opts *sgbucket.MutateInOptions) { + _, err := dataStore.WriteTombstoneWithXattrs(ctx, key, exp, 0, syncXattr(2), nil, false, opts) + require.NoError(t, err) + }, + }, + { + name: "WriteTombstoneWithXattrsTombstone", + state: tombstone, + takesExp: true, + takesOpts: true, + keepsOpExp: true, + op: func(t *testing.T, key string, cas uint64, exp uint32, opts *sgbucket.MutateInOptions) { + _, err := dataStore.WriteTombstoneWithXattrs(ctx, key, exp, cas, syncXattr(2), nil, false, opts) + require.NoError(t, err) + }, + }, + { + name: "WriteUpdateWithXattrs", + state: live, + takesExp: true, + takesOpts: true, + keepsOpExp: true, + op: func(t *testing.T, key string, _ uint64, exp uint32, opts *sgbucket.MutateInOptions) { + writeUpdateTombstone(t, key, exp, nil, opts) + }, + }, + { + name: "WriteUpdateWithXattrsMissing", + state: missing, + takesExp: true, + takesOpts: true, + keepsOpExp: true, + op: func(t *testing.T, key string, _ uint64, exp uint32, opts *sgbucket.MutateInOptions) { + writeUpdateTombstone(t, key, exp, nil, opts) + }, + }, + { + name: "WriteUpdateWithXattrsCallbackExpiry", + state: live, + takesExp: true, + takesOpts: true, + keepsOpExp: true, + op: func(t *testing.T, key string, _ uint64, exp uint32, opts *sgbucket.MutateInOptions) { + writeUpdateTombstone(t, key, 0, &exp, opts) + }, + }, + } + resurrections := map[string]func(t *testing.T, key string){ + "Set": func(t *testing.T, key string) { + require.NoError(t, dataStore.Set(ctx, key, 0, &sgbucket.UpsertOptions{PreserveExpiry: true}, map[string]any{"foo": "baz"})) + }, + "WriteWithXattrs": func(t *testing.T, key string) { + _, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"baz"}`), syncXattr(3), nil, &sgbucket.MutateInOptions{PreserveExpiry: true}) + require.NoError(t, err) + }, + "WriteResurrectionWithXattrs": func(t *testing.T, key string) { + _, err := dataStore.WriteResurrectionWithXattrs(ctx, key, 0, []byte(`{"foo":"baz"}`), syncXattr(3), &sgbucket.MutateInOptions{PreserveExpiry: true}) + require.NoError(t, err) + }, + } + type documentXattr struct { + Exptime uint32 `json:"exptime"` + Deleted bool `json:"deleted"` + } + getDocumentXattr := func(t *testing.T, key string) documentXattr { + xattrs, _, err := dataStore.GetXattrs(ctx, key, []string{VirtualDocumentXattr}) + require.NoError(t, err) + var document documentXattr + require.NoError(t, JSONUnmarshal(xattrs[VirtualDocumentXattr], &document)) + return document + } + requireExpiry := func(t *testing.T, expected, actual uint32, msg string) { + if expected == 0 { + require.Zero(t, actual, msg) + } else { + // Couchbase Server gets an absolute expiry as a duration, which can round it down. + require.InDelta(t, expected, actual, 2, msg) + } + } + for _, tc := range testCases { + docExpiries := []uint32{0, docExpiry} + if tc.state == missing { + docExpiries = []uint32{0} + } + opExpiries := []uint32{0} + if tc.takesExp { + opExpiries = append(opExpiries, opExpiry) + } + preserves := []bool{false} + if tc.takesOpts { + preserves = append(preserves, true) + } + for _, docExp := range docExpiries { + for _, opExp := range opExpiries { + for _, preserve := range preserves { + for resurrectionName, resurrect := range resurrections { + t.Run(fmt.Sprintf("%s/docExp=%t/opExp=%t/preserve=%t/%s", tc.name, docExp != 0, opExp != 0, preserve, resurrectionName), func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + var cas uint64 + if tc.state != missing { + var err error + cas, err = dataStore.WriteWithXattrs(ctx, key, docExp, 0, []byte(`{"foo":"bar"}`), syncXattr(1), nil, nil) + require.NoError(t, err) + exp, err := dataStore.GetExpiry(ctx, key) + require.NoError(t, err) + requireExpiry(t, docExp, exp, "live document expiry") + require.Equal(t, documentXattr{Exptime: exp}, getDocumentXattr(t, key)) + if tc.state == tombstone { + cas, err = dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + } + } + var opts *sgbucket.MutateInOptions + if preserve { + opts = &sgbucket.MutateInOptions{PreserveExpiry: true} + } + tc.op(t, key, cas, opExp, opts) + + var expected uint32 + if tc.keepsOpExp { + expected = opExp + } + _, _, err := dataStore.GetRaw(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + exp, err := dataStore.GetExpiry(ctx, key) + require.NoError(t, err) + requireExpiry(t, expected, exp, "tombstone expiry") + document := getDocumentXattr(t, key) + require.True(t, document.Deleted) + requireExpiry(t, expected, document.Exptime, "$document.exptime") + + resurrect(t, key) + exp, err = dataStore.GetExpiry(ctx, key) + require.NoError(t, err) + require.Zero(t, exp, "document written over the tombstone has an expiry") + }) + } + } + } + } + } +} + +// testNilWritesStoreBody checks that Set, SetRaw, Add, AddRaw and WriteWithXattrs with a nil value write a live +// document: an untyped nil writes a null JSON body and a nil []byte writes an empty body. +func testNilWritesStoreBody(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + + testCases := []struct { + name string + live bool // write over a live doc with a system and a user xattr + write func(t *testing.T, key string) + expectedBody string + }{ + { + name: "Set", + write: func(t *testing.T, key string) { require.NoError(t, dataStore.Set(ctx, key, 0, nil, nil)) }, + expectedBody: "null", + }, + { + name: "SetOverLive", + live: true, + write: func(t *testing.T, key string) { require.NoError(t, dataStore.Set(ctx, key, 0, nil, nil)) }, + expectedBody: "null", + }, + { + name: "SetRaw", + write: func(t *testing.T, key string) { require.NoError(t, dataStore.SetRaw(ctx, key, 0, nil, nil)) }, + }, + { + name: "Add", + write: func(t *testing.T, key string) { + added, err := dataStore.Add(ctx, key, 0, nil) + require.NoError(t, err) + require.True(t, added) + }, + expectedBody: "null", + }, + { + name: "AddRaw", + write: func(t *testing.T, key string) { + added, err := dataStore.AddRaw(ctx, key, 0, nil) + require.NoError(t, err) + require.True(t, added) + }, + }, + { + name: "WriteWithXattrsNilBody", + write: func(t *testing.T, key string) { + _, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, nil, map[string][]byte{SyncXattrName: []byte(`{"rev":"1-a"}`)}, nil, nil) + require.NoError(t, err) + }, + expectedBody: "null", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + if tc.live { + _, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), + map[string][]byte{SyncXattrName: []byte(`{"rev":"1-a"}`), "user": []byte(`{"u":1}`)}, nil, nil) + require.NoError(t, err) + } + tc.write(t, key) + + body, _, err := dataStore.GetRaw(ctx, key) + require.NoError(t, err) + require.Equal(t, tc.expectedBody, string(body)) + if tc.live { + xattrs, _, err := dataStore.GetXattrs(ctx, key, []string{SyncXattrName, "user"}) + require.NoError(t, err) + require.JSONEq(t, `{"rev":"1-a"}`, string(xattrs[SyncXattrName])) + require.JSONEq(t, `{"u":1}`, string(xattrs["user"])) + } + }) + } +} + +func testUpdateDelete(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + key := sgtest.SafeDocumentName(t, t.Name()) + require.NoError(t, dataStore.Set(ctx, key, 0, nil, []byte(`{"foo":"bar"}`))) + + _, err := dataStore.Update(ctx, key, 0, func([]byte) ([]byte, *uint32, bool, error) { + return nil, nil, true, nil + }) + require.NoError(t, err) + + _, _, err = dataStore.GetRaw(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) +} + +// testUpdateNilValueWithExpiry checks that a nil value with a new expiry replaces the body with an empty body. +func testUpdateNilValueWithExpiry(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + key := sgtest.SafeDocumentName(t, t.Name()) + require.NoError(t, dataStore.Set(ctx, key, 0, nil, []byte(`{"foo":"bar"}`))) + + exp := uint32(time.Now().Add(time.Hour).Unix()) + _, err := dataStore.Update(ctx, key, 0, func([]byte) ([]byte, *uint32, bool, error) { + return nil, &exp, false, nil + }) + require.NoError(t, err) + + body, _, err := dataStore.GetRaw(ctx, key) + require.NoError(t, err) + require.Empty(t, body) + gotExp, err := dataStore.GetExpiry(ctx, key) + require.NoError(t, err) + // Couchbase Server gets an absolute expiry as a duration, which can round it down. + require.InDelta(t, exp, gotExp, 2) +} + +// testWriteWithXattrsResurrectTombstoneZeroCas checks that a CAS of 0 writes a new body and xattr over a tombstone +// that kept its system xattr. +func testWriteWithXattrsResurrectTombstoneZeroCas(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + key := sgtest.SafeDocumentName(t, t.Name()) + + _, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"body":1}`), map[string][]byte{SyncXattrName: []byte(`{"seq":1}`)}, nil, nil) + require.NoError(t, err) + require.NoError(t, dataStore.Delete(ctx, key)) + + _, err = dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"body":2}`), map[string][]byte{SyncXattrName: []byte(`{"seq":2}`)}, nil, nil) + require.NoError(t, err) + + body, xattrs, _, err := dataStore.GetWithXattrs(ctx, key, []string{SyncXattrName}) + require.NoError(t, err) + require.JSONEq(t, `{"body":2}`, string(body)) + require.JSONEq(t, `{"seq":2}`, string(xattrs[SyncXattrName])) +} + +// testWriteTombstoneWithXattrsKeepBody checks that deleteBody=false with the current CAS of a live document only +// updates its xattrs and keeps the body. +func testWriteTombstoneWithXattrsKeepBody(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + key := sgtest.SafeDocumentName(t, t.Name()) + + cas, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"body":1}`), map[string][]byte{SyncXattrName: []byte(`{"seq":1}`)}, nil, nil) + require.NoError(t, err) + + _, err = dataStore.WriteTombstoneWithXattrs(ctx, key, 0, cas, map[string][]byte{SyncXattrName: []byte(`{"seq":2}`)}, nil, false, nil) + require.NoError(t, err) + + body, xattrs, _, err := dataStore.GetWithXattrs(ctx, key, []string{SyncXattrName}) + require.NoError(t, err) + require.JSONEq(t, `{"body":1}`, string(body)) + require.JSONEq(t, `{"seq":2}`, string(xattrs[SyncXattrName])) +} + +// testWriteUpdateWithXattrsDocDeletedDuringUpdate checks that WriteUpdateWithXattrs retries when the document is +// deleted between the read and the write. +func testWriteUpdateWithXattrsDocDeletedDuringUpdate(t *testing.T, dataStore sgbucket.DataStore) { + ctx := TestCtx(t) + key := sgtest.SafeDocumentName(t, t.Name()) + + firstCas, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"a":"body"}`), map[string][]byte{SyncXattrName: []byte(`{"seq":1}`)}, nil, nil) + require.NoError(t, err) + + calls := 0 + _, err = dataStore.WriteUpdateWithXattrs(ctx, key, []string{SyncXattrName}, 0, nil, nil, func(_ []byte, _ map[string][]byte, cas uint64) (sgbucket.UpdatedDoc, error) { + calls++ + if cas == firstCas { + _, err := dataStore.Remove(ctx, key, firstCas) + require.NoError(t, err) + } + return sgbucket.UpdatedDoc{Doc: []byte(`{"a":"updated body"}`), Xattrs: map[string][]byte{SyncXattrName: []byte(`{"seq":2}`)}}, nil + }) + require.NoError(t, err) + require.Equal(t, 2, calls) + + body, xattrs, _, err := dataStore.GetWithXattrs(ctx, key, []string{SyncXattrName}) + require.NoError(t, err) + require.JSONEq(t, `{"a":"updated body"}`, string(body)) + require.JSONEq(t, `{"seq":2}`, string(xattrs[SyncXattrName])) +} + +// testDeleteWithXattrsPathMismatch checks that DeleteWithXattrs treats a path through a scalar xattr like a missing +// xattr. +func testDeleteWithXattrsPathMismatch(t *testing.T, dataStore sgbucket.DataStore) { + const ( + success = "success" + notFound = "not found" + otherError = "other error" + ) + ctx := TestCtx(t) + + testCases := []struct { + name string + tombstone bool + xattrs map[string][]byte + deleteXattrs []string + expected string + }{ + { + name: "live doc, scalar xattr", + xattrs: map[string][]byte{SyncXattrName: []byte(`"scalar"`)}, + deleteXattrs: []string{SyncXattrName + ".rev"}, + expected: success, + }, + { + name: "live doc, scalar xattr and present xattr", + xattrs: map[string][]byte{SyncXattrName: []byte(`"scalar"`), "_xattr1": []byte(`{"seq":1}`)}, + deleteXattrs: []string{SyncXattrName + ".rev", "_xattr1"}, + expected: success, + }, + { + name: "tombstone, scalar xattr", + tombstone: true, + xattrs: map[string][]byte{SyncXattrName: []byte(`"scalar"`)}, + deleteXattrs: []string{SyncXattrName + ".rev"}, + expected: notFound, + }, + { + name: "tombstone, scalar xattr and present xattr", + tombstone: true, + xattrs: map[string][]byte{SyncXattrName: []byte(`"scalar"`), "_xattr1": []byte(`{"seq":1}`)}, + deleteXattrs: []string{SyncXattrName + ".rev", "_xattr1"}, + expected: otherError, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + key := sgtest.SafeDocumentName(t, t.Name()) + cas, err := dataStore.WriteWithXattrs(ctx, key, 0, 0, []byte(`{"foo":"bar"}`), tc.xattrs, nil, nil) + require.NoError(t, err) + if tc.tombstone { + _, err = dataStore.Remove(ctx, key, cas) + require.NoError(t, err) + } + + err = dataStore.DeleteWithXattrs(ctx, key, tc.deleteXattrs) + switch tc.expected { + case success: + require.NoError(t, err) + case notFound: + require.True(t, IsDocNotFoundError(err), "expected not found error, got %v", err) + case otherError: + require.Error(t, err) + require.False(t, IsDocNotFoundError(err), "expected an error other than not found, got %v", err) + } + + _, _, err = dataStore.GetRaw(ctx, key) + require.True(t, IsDocNotFoundError(err), "expected no document body, got %v", err) + for xattrName, value := range tc.xattrs { + xattrs, _, err := dataStore.GetXattrs(ctx, key, []string{xattrName}) + require.NoError(t, err) + require.JSONEq(t, string(value), string(xattrs[xattrName])) + } + }) + } +} + +// TestTombstonesAndNilWrites shares one bucket between its subtests to limit bucket creation on Couchbase Server. +func TestTombstonesAndNilWrites(t *testing.T) { + ctx := TestCtx(t) + bucket := GetTestBucket(t) + defer bucket.Close(ctx) + dataStore := bucket.GetSingleDataStore() + + for name, test := range map[string]func(t *testing.T, dataStore sgbucket.DataStore){ + "TombstoneRemovesUserXattrs": testTombstoneRemovesUserXattrs, + "WriteCasNil": testWriteCasNil, + "TombstoneFlagMatchesBody": testTombstoneFlagMatchesBody, + "UpdateByDocState": testUpdateByDocState, + "TombstoneExpiry": testTombstoneExpiry, + "NilWritesStoreBody": testNilWritesStoreBody, + "UpdateDelete": testUpdateDelete, + "UpdateNilValueWithExpiry": testUpdateNilValueWithExpiry, + "WriteWithXattrsResurrectTombstoneZeroCas": testWriteWithXattrsResurrectTombstoneZeroCas, + "WriteTombstoneWithXattrsKeepBody": testWriteTombstoneWithXattrsKeepBody, + "WriteUpdateWithXattrsDocDeletedDuringUpdate": testWriteUpdateWithXattrsDocDeletedDuringUpdate, + "DeleteWithXattrsPathMismatch": testDeleteWithXattrsPathMismatch, + } { + t.Run(name, func(t *testing.T) { test(t, dataStore) }) + } +} diff --git a/base/collection_xattr_test.go b/base/collection_xattr_test.go index 6c00d68185..e25f89bb1d 100644 --- a/base/collection_xattr_test.go +++ b/base/collection_xattr_test.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "maps" + "slices" "testing" sgbucket "github.com/couchbase/sg-bucket" @@ -903,7 +904,7 @@ func TestWriteTombstoneWithXattrs(t *testing.T) { body, xattrs, _, err := col.GetWithXattrs(ctx, docID, xattrKeys) require.NoError(t, err) - if test.finalBody == nil || UnitTestUrlIsWalrus() { + if test.finalBody == nil { require.Equal(t, "", string(body)) } else { require.JSONEq(t, string(test.finalBody), string(body)) @@ -1124,9 +1125,7 @@ func TestWriteUpdateWithXattrs(t *testing.T) { return } require.NoError(t, err) - if UnitTestUrlIsWalrus() && test.updatedDoc.IsTombstone { - require.Equal(t, "", string(body)) - } else if len(test.finalBody) > 0 { + if len(test.finalBody) > 0 { require.JSONEq(t, string(test.finalBody), string(body)) } else { require.Equal(t, string(test.finalBody), string(body)) @@ -1959,3 +1958,120 @@ func TestMetadataStoreSubdocStoreWriteOperations(t *testing.T) { require.Error(t, err) require.True(t, IsDocNotFoundError(err)) } + +// TestDeleteWithXattrs tests various combinations of deleting documents (with zero to many xattrs, some of which may not exist) across bucket implementations to ensure consistency in behavior. +func TestDeleteWithXattrs(t *testing.T) { + ctx := TestCtx(t) + bucket := GetTestBucket(t) + defer bucket.Close(ctx) + + col := bucket.GetSingleDataStore() + + tests := []struct { + name string + xattrsValues map[string][]byte + xattrsToDelete []string + expectedXattrs []string + }{ + { + name: "delete with no xattrs", + xattrsValues: nil, + xattrsToDelete: nil, + expectedXattrs: nil, + }, + { + name: "delete one only existing xattr", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"xattr1"}, + expectedXattrs: nil, + }, + { + name: "delete two existing xattrs", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`), "xattr2": []byte(`{"c":"d"}`)}, + xattrsToDelete: []string{"xattr1", "xattr2"}, + expectedXattrs: nil, + }, + { + name: "delete one xattr and one non-existing xattr", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"xattr1", "notexists"}, + expectedXattrs: nil, + }, + { + name: "delete one non-existing xattr", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"notexists"}, + expectedXattrs: nil, // user xattrs get removed along with regular delete... + }, + { + name: "create and delete system xattr", + xattrsValues: map[string][]byte{"_sync": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"_sync"}, + expectedXattrs: nil, + }, + { + name: "create and delete normal and system xattr", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`), "_sync": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"_sync"}, + expectedXattrs: nil, // user xattrs get removed along with regular delete... + }, + { + name: "create normal and system and do regular delete", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`), "_sync": []byte(`{"a":"b"}`)}, + xattrsToDelete: nil, + expectedXattrs: []string{"_sync"}, + }, + { + name: "create normal and system and delete system", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`), "_sync": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"_sync"}, + expectedXattrs: nil, // user xattrs get removed along with regular delete... + }, + { + name: "create two system xattrs and delete one", + xattrsValues: map[string][]byte{"_sync": []byte(`{"a":"b"}`), "_globalSync": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"_sync"}, + expectedXattrs: []string{"_globalSync"}, + }, + { + name: "create normal xattr and two system xattrs and delete one system xattr", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`), "_sync": []byte(`{"a":"b"}`), "_globalSync": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"_sync"}, + expectedXattrs: []string{"_globalSync"}, // user xattrs get removed along with regular delete... + }, + { + name: "delete non-existing system xattr on doc with no xattrs", + xattrsValues: nil, + xattrsToDelete: []string{"_systemXattr"}, + expectedXattrs: nil, + }, + { + name: "create xattr and delete non-existing system xattr", + xattrsValues: map[string][]byte{"xattr1": []byte(`{"a":"b"}`)}, + xattrsToDelete: []string{"_sync"}, + expectedXattrs: nil, // user xattrs get removed along with regular delete... + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + docID := t.Name() + + _, err := col.WriteWithXattrs(ctx, docID, 0, 0, []byte(`{"foo": "bar"}`), test.xattrsValues, nil, nil) + require.NoError(t, err) + + err = col.DeleteWithXattrs(ctx, docID, test.xattrsToDelete) + require.NoError(t, err) + + v, xv, _, err := col.GetWithXattrs(ctx, docID, slices.Collect(maps.Keys(test.xattrsValues))) + if len(test.expectedXattrs) == 0 { + assert.Errorf(t, err, "Expected document and all xattrs to be deleted, but it still exists (no error on get)") + assert.Truef(t, IsDocNotFoundError(err), "Expected document to be deleted, but got an error other than not found: %v", err) + } else { + require.NoErrorf(t, err, "Expected document or at least one xattr to still exist with xattrs after deletion") + } + assert.Nil(t, v, "Expected document to be deleted, but it still exists") + assert.Equal(t, test.expectedXattrs, slices.Collect(maps.Keys(xv)), "Expected xattrs to match expected values after deletion") + }) + } +} diff --git a/db/attachment_test.go b/db/attachment_test.go index 16a3723c1a..9b23e0b464 100644 --- a/db/attachment_test.go +++ b/db/attachment_test.go @@ -28,6 +28,7 @@ import ( "github.com/couchbase/sync_gateway/channels" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) func unmarshalBody(t *testing.T, j string) Body { @@ -2014,7 +2015,7 @@ func TestWritePathsPreserveUnmigratedAttachmentMetadata(t *testing.T) { _, err := collection.UpdateSyncFun(ctx, `function(doc){channel("preResync");}`) require.NoError(t, err) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) var rev1ID string if tc.create != nil { rev1ID = tc.create(t, ctx, collection, docID) diff --git a/db/delta_cache_test.go b/db/delta_cache_test.go index 6b55867f28..e315dd653d 100644 --- a/db/delta_cache_test.go +++ b/db/delta_cache_test.go @@ -19,6 +19,7 @@ import ( "github.com/couchbase/sync_gateway/channels" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // TestDeltaCacheStats verifies that DeltaCacheNumItems, DeltaCacheHit and DeltaCacheMiss @@ -44,7 +45,7 @@ func TestDeltaCacheStats(t *testing.T) { collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rev1ID, doc1, err := collection.Put(ctx, docID, Body{"foo": "bar"}) require.NoError(t, err) rev1CV := doc1.HLV.GetCurrentVersionString() @@ -97,7 +98,7 @@ func TestDeltaCacheStatsWithBypassRevisionCache(t *testing.T) { collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rev1ID, doc1, err := collection.Put(ctx, docID, Body{"foo": "bar"}) require.NoError(t, err) rev1CV := doc1.HLV.GetCurrentVersionString() @@ -148,7 +149,7 @@ func TestNumberBasedEvictionForDeltaCache(t *testing.T) { collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) _, doc, err := collection.Put(ctx, docID, Body{"test": "data"}) require.NoError(t, err) @@ -201,7 +202,7 @@ func TestUpdateDeltaWhenNoDeltaCacheInit(t *testing.T) { defer db.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rev1ID, doc1, err := collection.Put(ctx, docID, Body{"foo": "bar"}) require.NoError(t, err) rev1CV := doc1.HLV.GetCurrentVersionString() @@ -292,7 +293,7 @@ func TestGetDeltaStaleChannelAfterUserXattrUpdate(t *testing.T) { } t.Run("CV", func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()+"CV") + docID := sgtest.SafeDocumentName(t, t.Name()+"CV") rev1ID, doc1, err := collection.Put(ctx, docID, Body{"channels": []string{channelNew}, "foo": "bar"}) require.NoError(t, err) rev1CV := doc1.HLV.GetCurrentVersionString() @@ -305,7 +306,7 @@ func TestGetDeltaStaleChannelAfterUserXattrUpdate(t *testing.T) { }) t.Run("RevID", func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()+"revID") + docID := sgtest.SafeDocumentName(t, t.Name()+"revID") rev1ID, _, err := collection.Put(ctx, docID, Body{"channels": []string{channelNew}, "foo": "bar"}) require.NoError(t, err) @@ -341,7 +342,7 @@ func TestGetDeltaThreeRevisions(t *testing.T) { collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rev1ID, doc1, err := collection.Put(ctx, docID, Body{"foo": "bar"}) require.NoError(t, err) rev1CV := doc1.HLV.GetCurrentVersionString() @@ -425,7 +426,7 @@ func TestAccessCheckOnNonCurrentRevision(t *testing.T) { collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) collection.ChannelMapper = channels.NewChannelMapper(ctx, channels.DocChannelsSyncFunction, db.Options.JavascriptTimeout) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rev1ID, doc1, err := collection.Put(ctx, docID, Body{"channels": "A"}) require.NoError(t, err) rev1CV := doc1.HLV.GetCurrentVersionString() diff --git a/db/import_test.go b/db/import_test.go index a8806f3827..91661efd52 100644 --- a/db/import_test.go +++ b/db/import_test.go @@ -23,6 +23,7 @@ import ( "time" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" sgbucket "github.com/couchbase/sg-bucket" "github.com/couchbase/sync_gateway/base" @@ -113,7 +114,7 @@ func TestFeedImport(t *testing.T) { } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) db.CachedCCVEnabled.Store(testCase.eccv) for vBucket := range db.numVBuckets { db.CachedCCVStartingCas.Store(base.VBNo(vBucket), testCase.startingCAS) @@ -326,7 +327,7 @@ func TestOnDemandImport(t *testing.T) { } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) db.CachedCCVEnabled.Store(testCase.eccv) for vBucket := range db.numVBuckets { db.CachedCCVStartingCas.Store(base.VBNo(vBucket), testCase.startingCAS) @@ -1458,7 +1459,7 @@ func TestImportWithSyncCVAndNoVV(t *testing.T) { defer db.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) _, doc, err := collection.Put(ctx, docID, Body{"foo": "baz"}) require.NoError(t, err) @@ -2034,7 +2035,7 @@ func TestOnDemandImportMouRetargetedByConcurrentWrite(t *testing.T) { defer dbc.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, dbc) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) _, _, err := collection.Put(ctx, docID, Body{"value": 1234}) require.NoError(t, err) _, _, cas, err := collection.dataStore.GetWithXattrs(ctx, docID, []string{base.SyncXattrName}) @@ -2122,7 +2123,7 @@ func TestMouChainEndedByBodyWrite(t *testing.T) { defer dbc.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, dbc) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) _, _, err := collection.Put(ctx, docID, Body{"value": 1234}) require.NoError(t, err) @@ -2160,7 +2161,7 @@ func TestUserXattrImportMou(t *testing.T) { defer dbc.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, dbc) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) _, doc, err := collection.Put(ctx, docID, Body{"value": 1234}) require.NoError(t, err) revBeforeImport := doc.GetRevTreeID() @@ -2228,7 +2229,7 @@ func TestDuplicateImportLeavesMouAlone(t *testing.T) { defer dbc.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, dbc) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) _, doc, err := collection.Put(ctx, docID, Body{"value": 1234}) require.NoError(t, err) @@ -2404,7 +2405,7 @@ func TestWriteWithPreImportExistingDoc(t *testing.T) { defer dbCtx.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, dbCtx) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) prior.setup(t, ctx, collection, docID) // the read that imports, and hands back the snapshot it read before importing @@ -2449,7 +2450,7 @@ func TestOnDemandImportOfSupersededUserXattrWrite(t *testing.T) { defer dbCtx.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, dbCtx) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rev, doc, err := collection.Put(ctx, docID, Body{"foo": "sync gateway"}) require.NoError(t, err) _, err = collection.dataStore.UpdateXattrs(ctx, docID, 0, doc.Cas, map[string][]byte{userXattrKey: []byte(`{"channels": ["a"]}`)}, nil) diff --git a/db/revision_cache_test.go b/db/revision_cache_test.go index 14a5b67cb3..b3bce3b84b 100644 --- a/db/revision_cache_test.go +++ b/db/revision_cache_test.go @@ -24,6 +24,7 @@ import ( "github.com/couchbase/sync_gateway/base" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // testBackingStore always returns an empty doc at rev:"1-abc" in channel "*" except for docs not in 'notFoundDocIDs' @@ -2566,7 +2567,7 @@ func TestItemResidentInCacheBackupRevLoaded(t *testing.T) { defer db.Close(ctx) collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) var firstRevisionID, firstCV string _, doc1, err := collection.Put(ctx, docID, Body{"foo": "bar"}) require.NoError(t, err) @@ -2639,8 +2640,8 @@ func TestMultipleRevCacheItemsForSameDocs(t *testing.T) { collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) - docID := SafeDocumentName(t, t.Name()) - docID2 := SafeDocumentName(t, t.Name()+"_2") + docID := sgtest.SafeDocumentName(t, t.Name()) + docID2 := sgtest.SafeDocumentName(t, t.Name()+"_2") _, doc1, err := collection.Put(ctx, docID, Body{"foo": "bar"}) require.NoError(t, err) diff --git a/db/revtree_test.go b/db/revtree_test.go index 940c77bcf4..1e7c352f64 100644 --- a/db/revtree_test.go +++ b/db/revtree_test.go @@ -24,6 +24,7 @@ import ( "github.com/couchbase/sync_gateway/base" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // testmap is a revtree with a single branch @@ -2574,7 +2575,7 @@ func TestInvalidRevTreeRepairMou(t *testing.T) { collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, dbCtx) invalidRevTreeCount := dbCtx.DbStats.Database().InvalidRevTreeCount - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) bodyCas, bodyRevSeqNo := tc.setup(t, ctx, collection, docID) require.NotZero(t, bodyRevSeqNo) dbCtx.FlushRevisionCacheForTest() diff --git a/db/util_testing.go b/db/util_testing.go index 0434f132e0..785aacbc5c 100644 --- a/db/util_testing.go +++ b/db/util_testing.go @@ -1163,17 +1163,6 @@ func GetChangeEntryCV(t *testing.T, entry *ChangeEntry) Version { return Version{} } -// SafeDocumentName returns a document name free of any special characters for use in tests. -func SafeDocumentName(t *testing.T, name string) string { - t.Helper() - docName := strings.ToLower(name) - for _, c := range []string{" ", "<", ">", "/", "="} { - docName = strings.ReplaceAll(docName, c, "_") - } - require.Less(t, len(docName), 251, "Document name %s is too long, must be less than 251 characters", name) - return docName -} - // WaitForPendingChanges blocks until the change-cache has caught up with the latest writes to the database. Fails the // test harness if cache does not catch up. func (db *DatabaseContext) WaitForPendingChanges(t testing.TB) { diff --git a/go.mod b/go.mod index 09ac482ee1..0b593e9799 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/couchbase/sg-bucket v0.0.0-20260714130240-329df715dd61 github.com/couchbasedeps/fast-skiplist v0.0.0-20250722125747-e0dd031fe2ac github.com/couchbaselabs/go-fleecedelta v0.0.0-20220909152808-6d09efa7a338 - github.com/couchbaselabs/rosmar v0.0.0-20260929095922-bd5a47cd4a28 + github.com/couchbaselabs/rosmar v0.0.0-20261009154319-27bd760ee233 github.com/elastic/gosigar v0.14.4 github.com/felixge/fgprof v0.9.5 github.com/go-jose/go-jose/v4 v4.1.5 @@ -81,9 +81,11 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/sasha-s/go-deadlock v0.3.9 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect diff --git a/go.sum b/go.sum index 9706915a54..53cccf6194 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/couchbaselabs/gocaves/client v0.0.0-20250107114554-f96479220ae8 h1:MQ github.com/couchbaselabs/gocaves/client v0.0.0-20250107114554-f96479220ae8/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY= github.com/couchbaselabs/gocbconnstr/v2 v2.0.0 h1:HU9DlAYYWR69jQnLN6cpg0fh0hxW/8d5hnglCXXjW78= github.com/couchbaselabs/gocbconnstr/v2 v2.0.0/go.mod h1:o7T431UOfFVHDNvMBUmUxpHnhivwv7BziUao/nMl81E= -github.com/couchbaselabs/rosmar v0.0.0-20260929095922-bd5a47cd4a28 h1:SGupvd8GWfO8mcdXFP4LwoPBWk65rEzpFrzvRwUs6Qs= -github.com/couchbaselabs/rosmar v0.0.0-20260929095922-bd5a47cd4a28/go.mod h1:PrvPvpM+uDJ8XCiL/cHpyGTG3JBs78p2+GDA47TlkqA= +github.com/couchbaselabs/rosmar v0.0.0-20261009154319-27bd760ee233 h1:KQ2z2hSSgILqN0peRcVdTMdrpSAtC0Wd9XxZfJ+fGz8= +github.com/couchbaselabs/rosmar v0.0.0-20261009154319-27bd760ee233/go.mod h1:6W/jKlf0DWbh9oIh2A+oyXNM2mLgXJV20hERHFvVcdo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= @@ -161,6 +161,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -184,6 +186,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/samuel/go-metrics v0.0.0-20260131233133-1193a855cc76 h1:E1rpryM+NSpntRpFF8vl0uGNiAvR+JubF1d/OiLGOng= github.com/samuel/go-metrics v0.0.0-20260131233133-1193a855cc76/go.mod h1:ihmLZwy7rlySBgJHhYzMOT7stQqx3ZlOnbRI0LzSwI4= +github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w= +github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig= diff --git a/rest/api_test.go b/rest/api_test.go index b9f53b2c4f..282275a606 100644 --- a/rest/api_test.go +++ b/rest/api_test.go @@ -39,6 +39,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" "github.com/robertkrimen/otto/underscore" ) @@ -4405,7 +4406,7 @@ func TestInvalidRevTreeRestGetRepairsAndRenames(t *testing.T) { rt := NewRestTester(t, nil) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) collection, ctx := rt.GetSingleTestDatabaseCollectionWithUser() // 2-def sits at the same generation as its parent, so the repair renumbers it to 3-def diff --git a/rest/attachment_test.go b/rest/attachment_test.go index 98b0720b2f..b6d8e5adcb 100644 --- a/rest/attachment_test.go +++ b/rest/attachment_test.go @@ -30,6 +30,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) //from api_test.go @@ -2038,7 +2039,7 @@ func TestAttachmentRemovalWithConflicts(t *testing.T) { t.Run(tc.name, func(t *testing.T) { rt.GetDatabase().CachedCCVEnabled.Store(tc.eccv) - docID := db.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) // Create doc rev 1 version := rt.PutDoc(docID, `{"test": "x"}`) @@ -2204,7 +2205,7 @@ func TestAttachmentDeleteOnExpiry(t *testing.T) { lastImportCount := rt.GetDatabase().DbStats.SharedBucketImport().ImportCount.Value() rt.GetDatabase().CachedCCVEnabled.Store(tc.eccv) - docID := db.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) // Create doc with attachment and expiry resp := rt.SendAdminRequest("PUT", "/{{.keyspace}}/"+docID, `{"_attachments": {"hello.txt": {"data": "aGVsbG8gd29ybGQ="}}, "_exp": 1}`) RequireStatus(t, resp, http.StatusCreated) diff --git a/rest/blip_api_crud_test.go b/rest/blip_api_crud_test.go index ea60f187b5..101a099ac7 100644 --- a/rest/blip_api_crud_test.go +++ b/rest/blip_api_crud_test.go @@ -35,6 +35,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // This test performs the following steps against the Sync Gateway passive blip replicator: @@ -1206,7 +1207,7 @@ func TestBlipRevsThrottleUnblockedOnClose(t *testing.T) { // revs that can't get a slot, and so can only be unblocked by the connection closing numThrottledRevs = 3 ) - docIDPrefix := SafeDocumentName(t, t.Name()) + "-" + docIDPrefix := sgtest.SafeDocumentName(t, t.Name()) + "-" // Hold every rev that has a throttle slot inside its write until the end of the test, so that a // slot is never handed back to the revs that are waiting for one. @@ -2745,7 +2746,7 @@ func TestProcessRevIncrementsStat(t *testing.T) { remoteURL, err := url.Parse(remoteURLString) require.NoError(t, err) - replicationID := SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(activeCtx, &db.ActiveReplicatorConfig{ ID: replicationID, @@ -3587,7 +3588,7 @@ func TestManyChannelsRemovedOnDocUpdate(t *testing.T) { defer rt.Close() const alice = "alice" - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt.CreateUser(alice, []string{"*"}) sgVersion := rt.CreateDocNoHLV(docID, db.Body{"channels": []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T"}}).ExtractDocVersion() rt.WaitForPendingChanges() @@ -3659,7 +3660,7 @@ func TestPushHLVOntoLegacyRev(t *testing.T) { }) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) const alice = "alice" rt.CreateUser(alice, []string{"ABC"}) @@ -3770,7 +3771,7 @@ func TestBlipNoRevOnCorruptHistory(t *testing.T) { "seq": seq, })) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) mutateInOptions := db.DefaultMutateInOpts() _, err = rt.GetSingleDataStore().WriteWithXattrs( ctx, diff --git a/rest/blip_api_delta_sync_test.go b/rest/blip_api_delta_sync_test.go index 4f328fd620..58a04fc798 100644 --- a/rest/blip_api_delta_sync_test.go +++ b/rest/blip_api_delta_sync_test.go @@ -23,6 +23,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // TestBlipDeltaSyncPushAttachment tests updating a doc that has an attachment with a delta that doesn't modify the attachment. @@ -1344,7 +1345,7 @@ func TestDeltaReplicationWithBypassRevCacheAndInflightRevChanged(t *testing.T) { btcRunner.Run(func(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt := NewRestTester(t, rtConfig) defer rt.Close() @@ -1438,7 +1439,7 @@ func TestDeltaReplicationWithBypassRevCacheSendDeltaWhenInFlightRevChanged(t *te btcRunner.SkipSubtest[VersionVectorSubtestName] = true // only for rev tree clients will we always load backup rev isf one is available btcRunner.Run(func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt := NewRestTester(t, rtConfig) defer rt.Close() diff --git a/rest/blip_legacy_revid_test.go b/rest/blip_legacy_revid_test.go index 20d651d35f..807fc9d46b 100644 --- a/rest/blip_legacy_revid_test.go +++ b/rest/blip_legacy_revid_test.go @@ -25,6 +25,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // TestLegacyProposeChanges: @@ -319,7 +320,7 @@ func TestSendUnsolicitedRevWithRTEDerivedFromLocalRevID(t *testing.T) { defer bt.Close() rt := bt.restTester - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) doc := rt.CreateDocNoHLV(docID, db.Body{"key": "val"}) sgwVersion := doc.ExtractDocVersion() @@ -691,7 +692,7 @@ func TestDeltaSyncSendRevTreeHistoryWithClientHavingLegacyRev(t *testing.T) { }) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) client := btcRunner.NewBlipTesterClientOptsWithRT(rt, &BlipTesterClientOpts{ ClientDeltas: true, @@ -768,7 +769,7 @@ func TestDeltaSyncSendRevTreeHistoryWithHLVHistoryClientHavingLegacyRev(t *testi }) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) client := btcRunner.NewBlipTesterClientOptsWithRT(rt, &BlipTesterClientOpts{ ClientDeltas: true, @@ -856,7 +857,7 @@ func TestBothSidesLegacyRevDeltaSync(t *testing.T) { }) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) client := btcRunner.NewBlipTesterClientOptsWithRT(rt, &BlipTesterClientOpts{ ClientDeltas: true, @@ -1267,7 +1268,7 @@ func TestLegacyRevBlipTesterClient(t *testing.T) { btcRunner.StartPush(client.id) defer btcRunner.StopPush(client.id) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) cblDocVersion1 := btcRunner.AddRevTreeRev(client.id, docID, "1-abc", EmptyDocVersion(), []byte(`{"action": "create"}`)) rt.WaitForVersion(docID, cblDocVersion1) @@ -1281,7 +1282,7 @@ func TestLegacyRevBlipTesterClient(t *testing.T) { defer btcRunner.UnsubPullChanges(client.id) defer btcRunner.StopPush(client.id) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) dbc, ctx := rt.GetSingleTestDatabaseCollectionWithUser() revTreeID1, _ := dbc.CreateDocNoHLV(t, ctx, docID, db.Body{"action": "create"}) sgDocVersion1 := DocVersion{RevTreeID: revTreeID1} @@ -1297,7 +1298,7 @@ func TestLegacyRevBlipTesterClient(t *testing.T) { rt.WaitForVersion(docID, sgVersion3) }) t.Run("pull SG legacy rev 2-bcd, both sides have 1-abc", func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) dbc, ctx := rt.GetSingleTestDatabaseCollectionWithUser() sgVersion1, _ := dbc.CreateDocNoHLV(t, ctx, docID, db.Body{"action": "create"}) cblVersion1 := btcRunner.AddRevTreeRev(client.id, docID, sgVersion1, EmptyDocVersion(), []byte(`{"action": "create"}`)) @@ -1308,7 +1309,7 @@ func TestLegacyRevBlipTesterClient(t *testing.T) { btcRunner.WaitForVersion(client.id, docID, DocVersion{RevTreeID: sgVersion2}) }) t.Run("push CBL legacy rev 2-bcd, both sides have 1-abc", func(t *testing.T) { - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) dbc, ctx := rt.GetSingleTestDatabaseCollectionWithUser() sgVersion1, _ := dbc.CreateDocNoHLV(t, ctx, docID, db.Body{"action": "create"}) cblVersion1 := btcRunner.AddRevTreeRev(client.id, docID, sgVersion1, EmptyDocVersion(), []byte(`{"action": "create"}`)) @@ -1336,7 +1337,7 @@ func TestCBLPushEncodedCVDerivedFromSGWLocalRevID(t *testing.T) { btc := btcRunner.NewBlipTesterClientOptsWithRT(rt, nil) defer btc.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) // add legacy rev on SGW doc := rt.CreateDocNoHLV(docID, db.Body{"key": "val"}) @@ -1390,7 +1391,7 @@ func TestLegacyHistoryPushCreatesDuplicateGenerationRevs(t *testing.T) { client := btcRunner.NewBlipTesterClientOptsWithRT(rt, nil) defer client.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) // SGW holds a pre-upgrade document - rev tree only, no HLV - and the client holds the same // pre-upgrade revision. @@ -1445,7 +1446,7 @@ func TestInvalidRevTreePullRepairsAndRedelivers(t *testing.T) { rt := NewRestTester(t, &RestTesterConfig{GuestEnabled: true}) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) collection, ctx := rt.GetSingleTestDatabaseCollectionWithUser() // 3 revisions with a leaf at generation 2: encodeRevisions produces a _revisions list that @@ -1515,7 +1516,7 @@ func TestInvalidRevTreeQuietCaseRepairedBeforeSend(t *testing.T) { rt := NewRestTester(t, &RestTesterConfig{GuestEnabled: true}) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) collection, ctx := rt.GetSingleTestDatabaseCollectionWithUser() // gen 10 leaf on a 3 revision branch: 10 >= 3, so splitRevisionList accepts the encoding and @@ -1583,7 +1584,7 @@ func TestInvalidRevTreePullRenamedRevReplacementOrNoRev(t *testing.T) { rt := NewRestTester(t, &RestTesterConfig{GuestEnabled: true}) defer rt.Close() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) collection, ctx := rt.GetSingleTestDatabaseCollectionWithUser() // 2-def sits at the same generation as its parent, so the repair renumbers it to 3-def diff --git a/rest/cbl_conflict_test.go b/rest/cbl_conflict_test.go index c71e827c6e..ae0bcdbbed 100644 --- a/rest/cbl_conflict_test.go +++ b/rest/cbl_conflict_test.go @@ -23,6 +23,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // TestBlipConflictResolution covers CBL-side conflict resolution over BLIP, including the norev-clobber @@ -66,7 +67,7 @@ func testBlipPullConflict(t *testing.T, rt *RestTester) { alice = "alice" cblBody = `{"actor": "cbl"}` ) - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt.CreateUser(alice, []string{"*"}) sgVersion := rt.PutDoc(docID, `{"actor": "sg", "channels": ["shared"]}`) rt.WaitForPendingChanges() @@ -110,7 +111,7 @@ func testBlipPullConflictNoRevLosesBody(t *testing.T, rt *RestTester) { btcRunner.Run(func(t *testing.T) { const cblBody = `{"actor": "cbl"}` - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) btc := btcRunner.NewBlipTesterClientOptsWithRT(rt, nil) defer btc.Close() @@ -154,7 +155,7 @@ func testBlipNoRevIgnoredSingleChangesEntry(t *testing.T, rt *RestTester) { btcRunner.Run(func(t *testing.T) { const cblBody = `{"actor": "cbl"}` - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) btc := btcRunner.NewBlipTesterClientOptsWithRT(rt, nil) defer btc.Close() @@ -212,7 +213,7 @@ func testBlipNoRevOnCorruptHistoryDelta(t *testing.T, rt *RestTester) { defer btc.Close() ctx := rt.Context() - docID := SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) docRev1 := rt.CreateDocNoHLV(docID, db.Body{"delta": true, "channels": []string{channelA}}) btcRunner.StartOneshotPull(btc.id) btcRunner.WaitForVersion(btc.id, docID, DocVersion{RevTreeID: docRev1.GetRevTreeID()}) diff --git a/rest/importtest/import_test.go b/rest/importtest/import_test.go index bba728cbb7..7f33c3ff1d 100644 --- a/rest/importtest/import_test.go +++ b/rest/importtest/import_test.go @@ -2799,7 +2799,7 @@ func TestImportSameDbNameOnTwoBuckets(t *testing.T) { wg.Wait() require.Equal(t, rts[0].GetDatabase().Name, rts[1].GetDatabase().Name) - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) for _, rt := range rts { _, err := rt.GetSingleDataStore().AddRaw(ctx, docID, 0, []byte(`{"foo":"bar"}`)) require.NoError(t, err) diff --git a/rest/legacy_rev_test.go b/rest/legacy_rev_test.go index 5e5697b03d..f52d3d3025 100644 --- a/rest/legacy_rev_test.go +++ b/rest/legacy_rev_test.go @@ -16,6 +16,7 @@ import ( "github.com/couchbase/sync_gateway/base" "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // TestResyncLegacyRev makes sure that running resync on a legacy rev will send future blip messages as a legacy rev and not as an HLV @@ -31,7 +32,7 @@ func TestResyncLegacyRev(t *testing.T) { // default collection will use channel Name and named collection will use collection.Name rt.CreateUser(alice, []string{channelName, collection.Name}) - docID := db.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) doc := rt.CreateDocNoHLV(docID, db.Body{"channels": channelName}) btcRunner := NewBlipTesterClientRunner(t) diff --git a/rest/replicatortest/replicator_conflict_test.go b/rest/replicatortest/replicator_conflict_test.go index 5b8d081e93..e4822320dd 100644 --- a/rest/replicatortest/replicator_conflict_test.go +++ b/rest/replicatortest/replicator_conflict_test.go @@ -24,6 +24,7 @@ import ( "github.com/couchbase/sync_gateway/rest" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) func TestActiveReplicatorHLVConflictRemoteAndLocalWins(t *testing.T) { @@ -61,7 +62,7 @@ func TestActiveReplicatorHLVConflictRemoteAndLocalWins(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, testCase.conflictResType, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -208,7 +209,7 @@ func TestActiveReplicatorLWWDefaultResolver(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverDefault, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -463,7 +464,7 @@ func TestActiveReplicatorLocalWinsCases(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverLocalWins, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -763,7 +764,7 @@ func TestActiveReplicatorRemoteWinsCases(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverRemoteWins, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -995,7 +996,7 @@ func TestActiveReplicatorHLVConflictNoCommonMVPV(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, testCase.conflictResolverType, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -1190,7 +1191,7 @@ func TestActiveReplicatorAttachmentHandling(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, testCase.conflictResolverType, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -1337,7 +1338,7 @@ func TestActiveReplicatorHLVConflictWinnerIsTombstone(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, testCase.conflictResolverType, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -1492,7 +1493,7 @@ func TestActiveReplicatorInvalidCustomResolver(t *testing.T) { customConflictResolver, err := db.NewCustomConflictResolver(ctx1, resolver, rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -1642,7 +1643,7 @@ func TestActiveReplicatorHLVConflictCustom(t *testing.T) { customConflictResolver, err := db.NewCustomConflictResolver(ctx1, testCase.conflictResolver, rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePushAndPull, @@ -1762,7 +1763,7 @@ func TestActiveReplicatorHLVConflictWhenNonWinningRevHasMoreRevisions(t *testing resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, testCase.conflictResolverType, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -1897,7 +1898,7 @@ func TestActiveReplicatorHLVConflictLocalWinsWhenNonWinningRevHasLessRevisionsLo resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverLocalWins, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -2007,7 +2008,7 @@ func TestActiveReplicatorHLVConflictWithBothLocalAndRemoteTombstones(t *testing. resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverDefault, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -2079,7 +2080,7 @@ func TestActiveReplicatorConflictRemoveCVFromCache(t *testing.T) { require.NoError(t, err) ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt2Version := rt2.PutDoc(docID, `{"source":"rt2","channels":["alice"]}`) rt2.WaitForPendingChanges() rt1Version := rt1.PutDoc(docID, `{"source":"rt1","channels":["alice"]}`) @@ -2091,7 +2092,7 @@ func TestActiveReplicatorConflictRemoveCVFromCache(t *testing.T) { // add active rt to simulate two nodes on active cluster active := peers.AddActiveRT(t) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePushAndPull, @@ -2149,7 +2150,7 @@ func TestActiveReplicatorV4DefaultResolverWithTombstoneLocal(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) activeRT, passiveRT, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt1Version := activeRT.PutDoc(docID, `{"source":"activeRT","channels":["alice"]}`) // delete local version rt1Version = activeRT.DeleteDoc(docID, rt1Version) @@ -2159,7 +2160,7 @@ func TestActiveReplicatorV4DefaultResolverWithTombstoneLocal(t *testing.T) { rt2Version = passiveRT.UpdateDoc(docID, rt2Version, `{"source":"passiveRT-updated","channels":["alice"]}`) passiveRT.WaitForPendingChanges() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) activeRT.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "") activeRT.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -2200,7 +2201,7 @@ func TestActiveReplicatorV4DefaultResolverWithTombstoneRemote(t *testing.T) { sgrRunner.RunSubprotocolV4(func(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) activeRT, passiveRT, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt2Version := passiveRT.PutDoc(docID, `{"source":"passiveRT","channels":["alice"]}`) rt2Version = passiveRT.DeleteDoc(docID, rt2Version) @@ -2212,7 +2213,7 @@ func TestActiveReplicatorV4DefaultResolverWithTombstoneRemote(t *testing.T) { rt1Version = activeRT.UpdateDoc(docID, rt1Version, `{"source":"activeRT-updated","channels":["alice"]}`) activeRT.WaitForPendingChanges() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) activeRT.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "") activeRT.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -2388,7 +2389,7 @@ func TestISGRRepairCausesRevTreeConflictOnPeer(t *testing.T) { remoteURL, err := url.Parse(passiveDBURL) require.NoError(t, err) activeCtx := activeRT.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) plantUpgradeScenario(t, activeRT, passiveRT, docID) @@ -2407,7 +2408,7 @@ func TestISGRRepairCausesRevTreeConflictOnPeer(t *testing.T) { passiveRT.WaitForPendingChanges() pushOnly, err := db.NewActiveReplicator(activeCtx, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()+"push"), + ID: sgtest.SafeDocumentName(t, t.Name()+"push"), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: remoteURL, ActiveDB: &db.Database{DatabaseContext: activeRT.GetDatabase()}, @@ -2445,8 +2446,8 @@ func TestISGRRepairCausesRevTreeConflictOnPeer(t *testing.T) { require.True(t, strings.HasPrefix(passiveUpdateRev, "11-"), "got %q", passiveUpdateRev) passiveRT.WaitForPendingChanges() - activeRT.CreateReplication(rest.SafeDocumentName(t, t.Name()), passiveDBURL, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "") - activeRT.WaitForReplicationStatus(rest.SafeDocumentName(t, t.Name()), db.ReplicationStateRunning) + activeRT.CreateReplication(sgtest.SafeDocumentName(t, t.Name()), passiveDBURL, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "") + activeRT.WaitForReplicationStatus(sgtest.SafeDocumentName(t, t.Name()), db.ReplicationStateRunning) // passive version wins requireRepairConvergedOnPeerBranch(t, activeRT, passiveRT, docID, passiveUpdateRev) @@ -2461,7 +2462,7 @@ func TestISGRRepairCausesRevTreeConflictOnPeer(t *testing.T) { sgrRunner.RunSubprotocolV3(func(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) activeRT, passiveRT, passiveDBURL := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) plantUpgradeScenario(t, activeRT, passiveRT, docID) @@ -2475,8 +2476,8 @@ func TestISGRRepairCausesRevTreeConflictOnPeer(t *testing.T) { passiveRT.WaitForPendingChanges() require.Equal(t, int64(0), activeRT.GetDatabase().DbStats.Database().InvalidRevTreeCount.Value()) - activeRT.CreateReplication(rest.SafeDocumentName(t, t.Name()), passiveDBURL, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "") - activeRT.WaitForReplicationStatus(rest.SafeDocumentName(t, t.Name()), db.ReplicationStateRunning) + activeRT.CreateReplication(sgtest.SafeDocumentName(t, t.Name()), passiveDBURL, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "") + activeRT.WaitForReplicationStatus(sgtest.SafeDocumentName(t, t.Name()), db.ReplicationStateRunning) // passive version wins requireRepairConvergedOnPeerBranch(t, activeRT, passiveRT, docID, passiveUpdateRev) diff --git a/rest/replicatortest/replicator_no_race_test.go b/rest/replicatortest/replicator_no_race_test.go index d6a5d37610..089abb20db 100644 --- a/rest/replicatortest/replicator_no_race_test.go +++ b/rest/replicatortest/replicator_no_race_test.go @@ -28,6 +28,7 @@ import ( "github.com/couchbase/sync_gateway/rest" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // TestReplicationHeartbeatRemovalPushWithConfigReload is a push variant of TestReplicationHeartbeatRemoval @@ -49,8 +50,8 @@ func TestReplicationHeartbeatRemovalPushWithConfigReload(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) activeRT, remoteRT, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL - docABC1 := rest.SafeDocumentName(t, t.Name()+"ABC1") - docDEF1 := rest.SafeDocumentName(t, t.Name()+"DEF1") + docABC1 := sgtest.SafeDocumentName(t, t.Name()+"ABC1") + docDEF1 := sgtest.SafeDocumentName(t, t.Name()+"DEF1") _ = activeRT.PutDoc(docABC1, `{"source":"activeRT","channels":["ABC"]}`) _ = activeRT.PutDoc(docDEF1, `{"source":"activeRT","channels":["DEF"]}`) activeRT.WaitForPendingChanges() @@ -118,9 +119,9 @@ func TestReplicationHeartbeatRemovalPushWithConfigReload(t *testing.T) { activeRT.WaitForAssignedReplications(1) activeRT2.WaitForAssignedReplications(1) - docABC2 := rest.SafeDocumentName(t, t.Name()+"ABC2") + docABC2 := sgtest.SafeDocumentName(t, t.Name()+"ABC2") _ = activeRT.PutDoc(docABC2, `{"source":"activeRT","channels":["ABC"]}`) - docDEF2 := rest.SafeDocumentName(t, t.Name()+"DEF2") + docDEF2 := sgtest.SafeDocumentName(t, t.Name()+"DEF2") _ = activeRT.PutDoc(docDEF2, `{"source":"activeRT","channels":["DEF"]}`) // Post-reload writes must still reach the remote, whichever node now owns the replications. diff --git a/rest/replicatortest/replicator_revtree_test.go b/rest/replicatortest/replicator_revtree_test.go index 8097146625..51935e161b 100644 --- a/rest/replicatortest/replicator_revtree_test.go +++ b/rest/replicatortest/replicator_revtree_test.go @@ -21,6 +21,7 @@ import ( "github.com/couchbase/sync_gateway/rest" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) func TestActiveReplicatorRevTreeReconciliation(t *testing.T) { @@ -63,7 +64,7 @@ func TestActiveReplicatorRevTreeReconciliation(t *testing.T) { rt1.WaitForPendingChanges() } - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: tc.replicationType, @@ -174,7 +175,7 @@ func TestActiveReplicatorNoHLVConflictConflictInRevTree(t *testing.T) { remoteURL, err := url.Parse(remoteURLString) require.NoError(t, err) ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rt1collection, rt1ctx := rt1.GetSingleTestDatabaseCollectionWithUser() @@ -184,7 +185,7 @@ func TestActiveReplicatorNoHLVConflictConflictInRevTree(t *testing.T) { resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverRemoteWins, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -306,7 +307,7 @@ func TestActiveReplicatorRevtreeLargeDiffInSize(t *testing.T) { rt1.WaitForPendingChanges() } - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: tc.replicationType, diff --git a/rest/replicatortest/replicator_test.go b/rest/replicatortest/replicator_test.go index b80d5957e3..449c68a8f6 100644 --- a/rest/replicatortest/replicator_test.go +++ b/rest/replicatortest/replicator_test.go @@ -30,6 +30,7 @@ import ( "github.com/couchbase/gocb/v2" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" "github.com/google/uuid" "github.com/couchbase/sync_gateway/auth" @@ -479,11 +480,11 @@ func TestPushReplicationAPI(t *testing.T) { rt1, rt2, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL // Create doc1 on rt1 - docID1 := rest.SafeDocumentName(t, t.Name()+"rt1doc") + docID1 := sgtest.SafeDocumentName(t, t.Name()+"rt1doc") _ = rt1.PutDoc(docID1, `{"source":"rt1","channels":["alice"]}`) // Create push replication, verify running - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePush, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -496,7 +497,7 @@ func TestPushReplicationAPI(t *testing.T) { assert.Equal(t, "rt1", doc1Body["source"]) // Create doc2 on rt1 - docID2 := rest.SafeDocumentName(t, t.Name()+"rt1doc2") + docID2 := sgtest.SafeDocumentName(t, t.Name()+"rt1doc2") _ = rt2.PutDoc(docID2, `{"source":"rt1","channels":["alice"]}`) // wait for doc2 to arrive at rt2 @@ -560,11 +561,11 @@ func TestPullReplicationAPI(t *testing.T) { rt1, rt2, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL // Create doc1 on rt2 - docID1 := rest.SafeDocumentName(t, t.Name()+"rt2doc") + docID1 := sgtest.SafeDocumentName(t, t.Name()+"rt2doc") _ = rt2.PutDoc(docID1, `{"source":"rt2","channels":["alice"]}`) // Create pull replication, verify running - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -577,7 +578,7 @@ func TestPullReplicationAPI(t *testing.T) { assert.Equal(t, "rt2", doc1Body["source"]) // Create doc2 on rt2 - docID2 := rest.SafeDocumentName(t, t.Name()+"rt2doc2") + docID2 := sgtest.SafeDocumentName(t, t.Name()+"rt2doc2") _ = rt2.PutDoc(docID2, `{"source":"rt2","channels":["alice"]}`) // wait for new document to arrive at rt1 @@ -609,17 +610,17 @@ func TestDisableConcurrentReplicationLimitDuringReplications(t *testing.T) { } // create two replications to take us to the limit - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) - replicationID = rest.SafeDocumentName(t, t.Name()+"1") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"1") rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) rt1.WaitForActiveReplicatorInitialization(2) // try create a new replication to take it beyond the threshold set by runtime config call // assert it enter error state - replicationID = rest.SafeDocumentName(t, t.Name()+"2") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"2") rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateReconnecting) @@ -630,7 +631,7 @@ func TestDisableConcurrentReplicationLimitDuringReplications(t *testing.T) { // assert the replications aren't killed as result of change in limit rt2.WaitForActiveReplicatorCount(2) // assert we still can create a new replication given that originally the limit was 2 replications - replicationID = rest.SafeDocumentName(t, t.Name()+"3") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"3") rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) }) @@ -656,17 +657,17 @@ func TestConcurrentReplicationLimitContinuous(t *testing.T) { } // create two replications to take us to the limit - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) - replicationID = rest.SafeDocumentName(t, t.Name()+"1") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"1") rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) rt1.WaitForActiveReplicatorInitialization(2) // try create a new replication to take it beyond the threshold set by runtime config call // assert it enter error state - replicationID = rest.SafeDocumentName(t, t.Name()+"2") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"2") rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateReconnecting) @@ -677,20 +678,20 @@ func TestConcurrentReplicationLimitContinuous(t *testing.T) { // assert the replications aren't killed as result of change in limit rt2.WaitForActiveReplicatorCount(2) // assert we still can't create a new replication - replicationID = rest.SafeDocumentName(t, t.Name()+"3") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"3") rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateReconnecting) // stop one of the replicators currently running - replicationID = rest.SafeDocumentName(t, t.Name()+"1") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"1") resp = rt1.SendAdminRequest(http.MethodPut, "/{{.db}}/_replicationStatus/"+replicationID+"?action=stop", "") rest.RequireStatus(t, resp, http.StatusOK) - rt1.WaitForReplicationStatus(rest.SafeDocumentName(t, t.Name()+"1"), db.ReplicationStateStopped) + rt1.WaitForReplicationStatus(sgtest.SafeDocumentName(t, t.Name()+"1"), db.ReplicationStateStopped) // assert the count has been decremented rt2.WaitForActiveReplicatorCount(1) // assert we still can't create new replication (new limit is 1) - replicationID = rest.SafeDocumentName(t, t.Name()+"4") + replicationID = sgtest.SafeDocumentName(t, t.Name()+"4") rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateReconnecting) }) @@ -715,11 +716,11 @@ func TestReplicationStatusActions(t *testing.T) { rt1, rt2, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL // Create doc1 on rt2 - docID1 := rest.SafeDocumentName(t, t.Name()+"rt2doc") + docID1 := sgtest.SafeDocumentName(t, t.Name()+"rt2doc") _ = rt2.PutDoc(docID1, `{"source":"rt2","channels":["alice"]}`) // Create pull replication, verify running - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -751,7 +752,7 @@ func TestReplicationStatusActions(t *testing.T) { assert.Equal(t, "rt2", doc1Body["source"]) // Create doc2 on rt2 - docID2 := rest.SafeDocumentName(t, t.Name()+"rt2doc2") + docID2 := sgtest.SafeDocumentName(t, t.Name()+"rt2doc2") _ = rt2.PutDoc(docID2, `{"source":"rt2","channels":["alice"]}`) // wait for new document to arrive at rt1 @@ -812,7 +813,7 @@ func TestReplicationRebalanceToZeroNodes(t *testing.T) { defer srv.Close() // Put replication on remote RT where sg replicate is off, so will not get assigned a node - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) remoteRT.CreateReplication(replicationID, activeDBURL.String(), db.ActiveReplicatorTypePush, nil, false, db.ConflictResolverDefault, "") remoteRT.WaitForAssignedReplications(0) @@ -844,8 +845,8 @@ func TestReplicationRebalancePull(t *testing.T) { activeRT, remoteRT, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL // Create docs on remote - docABC1 := rest.SafeDocumentName(t, t.Name()+"ABC1") - docDEF1 := rest.SafeDocumentName(t, t.Name()+"DEF1") + docABC1 := sgtest.SafeDocumentName(t, t.Name()+"ABC1") + docDEF1 := sgtest.SafeDocumentName(t, t.Name()+"DEF1") _ = remoteRT.PutDoc(docABC1, `{"source":"remoteRT","channels":["ABC"]}`) _ = remoteRT.PutDoc(docDEF1, `{"source":"remoteRT","channels":["DEF"]}`) remoteRT.WaitForPendingChanges() @@ -884,9 +885,9 @@ func TestReplicationRebalancePull(t *testing.T) { t.Logf("==============replication rebalance is done================") // Create additional docs on remoteRT - docABC2 := rest.SafeDocumentName(t, t.Name()+"ABC2") + docABC2 := sgtest.SafeDocumentName(t, t.Name()+"ABC2") _ = remoteRT.PutDoc(docABC2, `{"source":"remoteRT","channels":["ABC"]}`) - docDEF2 := rest.SafeDocumentName(t, t.Name()+"DEF2") + docDEF2 := sgtest.SafeDocumentName(t, t.Name()+"DEF2") _ = remoteRT.PutDoc(docDEF2, `{"source":"remoteRT","channels":["DEF"]}`) // wait for new documents to arrive at activeRT @@ -938,8 +939,8 @@ func TestReplicationRebalancePush(t *testing.T) { activeRT, remoteRT, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL // Create docs on active - docABC1 := rest.SafeDocumentName(t, t.Name()+"ABC1") - docDEF1 := rest.SafeDocumentName(t, t.Name()+"DEF1") + docABC1 := sgtest.SafeDocumentName(t, t.Name()+"ABC1") + docDEF1 := sgtest.SafeDocumentName(t, t.Name()+"DEF1") _ = activeRT.PutDoc(docABC1, `{"source":"activeRT","channels":["ABC"]}`) _ = activeRT.PutDoc(docDEF1, `{"source":"activeRT","channels":["DEF"]}`) @@ -979,9 +980,9 @@ func TestReplicationRebalancePush(t *testing.T) { activeRT2.WaitForAssignedReplications(1) // Create additional docs on local - docABC2 := rest.SafeDocumentName(t, t.Name()+"ABC2") + docABC2 := sgtest.SafeDocumentName(t, t.Name()+"ABC2") _ = activeRT.PutDoc(docABC2, `{"source":"activeRT","channels":["ABC"]}`) - docDEF2 := rest.SafeDocumentName(t, t.Name()+"DEF2") + docDEF2 := sgtest.SafeDocumentName(t, t.Name()+"DEF2") _ = activeRT.PutDoc(docDEF2, `{"source":"activeRT","channels":["DEF"]}`) // wait for new documents to arrive at remote @@ -1026,7 +1027,7 @@ func TestPullOneshotReplicationAPI(t *testing.T) { // Create 20 docs on rt2 docCount := 20 docIDs := make([]string, 20) - prefix := rest.SafeDocumentName(t, t.Name()) + prefix := sgtest.SafeDocumentName(t, t.Name()) for i := range 20 { docID := fmt.Sprintf("%s%s%d", prefix, "rt2doc", i) _ = remoteRT.PutDoc(docID, `{"source":"rt2","channels":["alice"]}`) @@ -1036,7 +1037,7 @@ func TestPullOneshotReplicationAPI(t *testing.T) { remoteRT.WaitForPendingChanges() // Create oneshot replication, verify running - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) activeRT.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePull, nil, false, db.ConflictResolverDefault, "") activeRT.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -1090,8 +1091,8 @@ func TestReplicationConcurrentPush(t *testing.T) { activeRT.WaitForActiveReplicatorInitialization(2) // Create docs on active - docAllChannels1 := rest.SafeDocumentName(t, t.Name()+"All1") - docAllChannels2 := rest.SafeDocumentName(t, t.Name()+"All2") + docAllChannels1 := sgtest.SafeDocumentName(t, t.Name()+"All1") + docAllChannels2 := sgtest.SafeDocumentName(t, t.Name()+"All2") _ = activeRT.PutDoc(docAllChannels1, `{"source":"activeRT1","channels":["ABC","DEF"]}`) _ = activeRT.PutDoc(docAllChannels2, `{"source":"activeRT2","channels":["ABC","DEF"]}`) @@ -1773,8 +1774,8 @@ func TestReplicationHeartbeatRemoval(t *testing.T) { activeRT, remoteRT, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL // Create docs on remote - docABC1 := rest.SafeDocumentName(t, t.Name()+"ABC1") - docDEF1 := rest.SafeDocumentName(t, t.Name()+"DEF1") + docABC1 := sgtest.SafeDocumentName(t, t.Name()+"ABC1") + docDEF1 := sgtest.SafeDocumentName(t, t.Name()+"DEF1") _ = remoteRT.PutDoc(docABC1, `{"source":"remoteRT","channels":["ABC"]}`) _ = remoteRT.PutDoc(docDEF1, `{"source":"remoteRT","channels":["DEF"]}`) @@ -1800,9 +1801,9 @@ func TestReplicationHeartbeatRemoval(t *testing.T) { activeRT2.WaitForAssignedReplications(1) // Create additional docs on remoteRT - docABC2 := rest.SafeDocumentName(t, t.Name()+"ABC2") + docABC2 := sgtest.SafeDocumentName(t, t.Name()+"ABC2") _ = remoteRT.PutDoc(docABC2, `{"source":"remoteRT","channels":["ABC"]}`) - docDEF2 := rest.SafeDocumentName(t, t.Name()+"DEF2") + docDEF2 := sgtest.SafeDocumentName(t, t.Name()+"DEF2") _ = remoteRT.PutDoc(docDEF2, `{"source":"remoteRT","channels":["DEF"]}`) // wait for new documents to arrive at activeRT @@ -1838,9 +1839,9 @@ func TestReplicationHeartbeatRemoval(t *testing.T) { activeRT2.WaitForAssignedReplications(1) // Add more docs to remote, to validate rebalanced replications are running - docABC3 := rest.SafeDocumentName(t, t.Name()+"ABC3") + docABC3 := sgtest.SafeDocumentName(t, t.Name()+"ABC3") _ = remoteRT.PutDoc(docABC3, `{"source":"remoteRT","channels":["ABC"]}`) - docDEF3 := rest.SafeDocumentName(t, t.Name()+"DEF3") + docDEF3 := sgtest.SafeDocumentName(t, t.Name()+"DEF3") _ = remoteRT.PutDoc(docDEF3, `{"source":"remoteRT","channels":["DEF"]}`) changesResults = activeRT.WaitForChanges(2, "/{{.keyspace}}/_changes?since="+changesResults.Last_Seq.String(), "", true) @@ -1929,11 +1930,11 @@ func TestTakeDbOfflineOngoingPushReplication(t *testing.T) { rt1, rt2, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL // Create doc1 on rt1 - docID1 := rest.SafeDocumentName(t, t.Name()+"rt1doc") + docID1 := sgtest.SafeDocumentName(t, t.Name()+"rt1doc") _ = rt1.PutDoc(docID1, `{"source":"rt1","channels":["alice"]}`) // Create push replication, verify running - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePush, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -1957,10 +1958,10 @@ func TestPushReplicationAPIUpdateDatabase(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) rt1, rt2, remoteURLString := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL - docID := rest.SafeDocumentName(t, t.Name()+"rt1doc") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt1doc") version := rt1.PutDoc(docID, `{"source":"rt1","channels":["alice"]}`) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) rt1.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePush, nil, true, db.ConflictResolverDefault, "") rt1.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) sgrRunner.WaitForDocReplicated(docID, rt1, rt2, version) @@ -1981,7 +1982,7 @@ func TestPushReplicationAPIUpdateDatabase(t *testing.T) { }() // Start creating documents in the background on rt1 for the replicator to push to rt2 - docPrefix := rest.SafeDocumentName(t, t.Name()) + docPrefix := sgtest.SafeDocumentName(t, t.Name()) wg.Go(func() { for i := 0; shouldCreateDocs.IsTrue(); i++ { docID := fmt.Sprintf("%s-doc%d", docPrefix, i) @@ -2098,7 +2099,7 @@ func TestActiveReplicatorPullBasic(t *testing.T) { const username = "AL_1c.e-@" rt2.CreateUser(username, []string{username}) - docID := rest.SafeDocumentName(t, t.Name()) + "rt2doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt2doc1" version := rt2.PutDoc(docID, `{"source":"rt2","channels":["`+username+`"]}`) rt2collection, rt2ctx := rt2.GetSingleTestDatabaseCollection() @@ -2107,7 +2108,7 @@ func TestActiveReplicatorPullBasic(t *testing.T) { ctx1 := rt1.Context() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -2194,7 +2195,7 @@ func TestActiveReplicatorPullSkippedSequence(t *testing.T) { dbstats := dbReplicatorStats(t, rt1.GetDatabase()) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -2211,7 +2212,7 @@ func TestActiveReplicatorPullSkippedSequence(t *testing.T) { require.NoError(t, err) defer func() { assert.NoError(t, ar.Stop()) }() - docIDPrefix := rest.SafeDocumentName(t, t.Name()+"rt2doc") + docIDPrefix := sgtest.SafeDocumentName(t, t.Name()+"rt2doc") docID1 := docIDPrefix + "1" doc1Version := rt2.PutDoc(docID1, `{"source":"rt2","channels":["`+username+`"]}`) @@ -2619,12 +2620,12 @@ func TestActiveReplicatorPullAttachments(t *testing.T) { attachment := `"_attachments":{"hi.txt":{"data":"aGk=","content_type":"text/plain"}}` - docID := rest.SafeDocumentName(t, t.Name()+"rt2doc1") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt2doc1") version := rt2.PutDoc(docID, `{"source":"rt2","doc_num":1,`+attachment+`,"channels":["alice"]}`) ctx1 := rt1.Context() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, Direction: db.ActiveReplicatorTypePull, @@ -2659,7 +2660,7 @@ func TestActiveReplicatorPullAttachments(t *testing.T) { assert.Equal(t, int64(1), ar.Pull.GetStats().GetAttachment.Value()) - docID = rest.SafeDocumentName(t, t.Name()+"rt2doc2") + docID = sgtest.SafeDocumentName(t, t.Name()+"rt2doc2") version = rt2.PutDoc(docID, `{"source":"rt2","doc_num":2,`+attachment+`,"channels":["alice"]}`) // wait for the new document written to rt2 to arrive at rt1 @@ -2891,7 +2892,7 @@ func TestActiveReplicatorPullFromCheckpoint(t *testing.T) { }) rt1, rt2 := peers.ActiveRT, peers.PassiveRT // Create first batch of docs - docIDPrefix := rest.SafeDocumentName(t, t.Name()) + "rt2doc" + docIDPrefix := sgtest.SafeDocumentName(t, t.Name()) + "rt2doc" for i := range numRT2DocsInitial { resp := rt2.SendAdminRequest(http.MethodPut, fmt.Sprintf("/{{.keyspace}}/%s%d", docIDPrefix, i), `{"source":"rt2","channels":["alice"]}`) rest.RequireStatus(t, resp, http.StatusCreated) @@ -2899,7 +2900,7 @@ func TestActiveReplicatorPullFromCheckpoint(t *testing.T) { ctx1 := rt1.Context() arConfig := db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePull, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3054,7 +3055,7 @@ func TestActiveReplicatorPullFromCheckpointIgnored(t *testing.T) { ctx1 := rt1.Context() // Create first batch of docs - docIDPrefix := rest.SafeDocumentName(t, t.Name()) + "doc" + docIDPrefix := sgtest.SafeDocumentName(t, t.Name()) + "doc" for i := range numRT2DocsInitial { rt1Version := rt1.PutDoc(fmt.Sprintf("%s%d", docIDPrefix, i), `{"channels":["alice"]}`) rt2Version := rt2.PutDoc(fmt.Sprintf("%s%d", docIDPrefix, i), `{"channels":["alice"]}`) @@ -3186,12 +3187,12 @@ func TestActiveReplicatorPullOneshot(t *testing.T) { }) rt1, rt2 := peers.ActiveRT, peers.PassiveRT - docID := rest.SafeDocumentName(t, t.Name()) + "rt2doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt2doc1" rt2.PutDoc(docID, `{"source":"rt2","channels":["alice"]}`) ctx1 := rt1.Context() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePull, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3235,7 +3236,7 @@ func TestActiveReplicatorPushBasic(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + "rt1doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt1doc1" version := rt1.PutDoc(docID, `{"source":"rt1","channels":["alice"]}`) rt1.WaitForPendingChanges() @@ -3244,7 +3245,7 @@ func TestActiveReplicatorPushBasic(t *testing.T) { assert.NoError(t, err) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3295,11 +3296,11 @@ func TestActiveReplicatorPushAttachments(t *testing.T) { ctx1 := rt1.Context() attachment := `"_attachments":{"hi.txt":{"data":"aGk=","content_type":"text/plain"}}` - docID := rest.SafeDocumentName(t, t.Name()+"rt1doc1") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt1doc1") version := rt1.PutDoc(docID, `{"source":"rt1","doc_num":1,`+attachment+`,"channels":["alice"]}`) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3324,7 +3325,7 @@ func TestActiveReplicatorPushAttachments(t *testing.T) { assert.Equal(t, int64(1), ar.Push.GetStats().HandleGetAttachment.Value()) - docID = rest.SafeDocumentName(t, t.Name()+"rt1doc2") + docID = sgtest.SafeDocumentName(t, t.Name()+"rt1doc2") version = rt1.PutDoc(docID, `{"source":"rt1","doc_num":2,`+attachment+`,"channels":["alice"]}`) // wait for the new document written to rt1 to arrive at rt2 @@ -3367,13 +3368,13 @@ func TestActiveReplicatorPushFromCheckpoint(t *testing.T) { ctx1 := rt1.Context() // Create first batch of docs - docIDPrefix := rest.SafeDocumentName(t, t.Name()) + "rt2doc" + docIDPrefix := sgtest.SafeDocumentName(t, t.Name()) + "rt2doc" for i := range numRT1DocsInitial { rt1.PutDoc(fmt.Sprintf("%s%d", docIDPrefix, i), `{"source":"rt1","channels":["alice"]}`) } arConfig := db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3508,7 +3509,7 @@ func TestActiveReplicatorEdgeCheckpointNameCollisions(t *testing.T) { }) rt2 := peers.PassiveRT // Create first batch of docs - docIDPrefix := rest.SafeDocumentName(t, t.Name()) + "rt1doc" + docIDPrefix := sgtest.SafeDocumentName(t, t.Name()) + "rt1doc" for i := range numRT1DocsInitial { rt2.PutDoc(fmt.Sprintf("%s%d", docIDPrefix, i), `{"source":"rt1","channels":["alice"]}`) } @@ -3668,7 +3669,7 @@ func TestActiveReplicatorPushOneshot(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) rt1, rt2 := peers.ActiveRT, peers.PassiveRT - docID := rest.SafeDocumentName(t, t.Name()) + "rt1doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt1doc1" version := rt1.PutDoc(docID, `{"source":"rt1","channels":["alice"]}`) rt1.WaitForPendingChanges() @@ -3678,7 +3679,7 @@ func TestActiveReplicatorPushOneshot(t *testing.T) { ctx1 := rt1.Context() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3731,12 +3732,12 @@ func TestActiveReplicatorPullTombstone(t *testing.T) { }) rt1, rt2 := peers.ActiveRT, peers.PassiveRT - docID := rest.SafeDocumentName(t, t.Name()) + "rt2doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt2doc1" version := rt2.PutDoc(docID, `{"source":"rt2","channels":["alice"]}`) ctx1 := rt1.Context() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePull, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3795,13 +3796,13 @@ func TestActiveReplicatorPullPurgeOnRemoval(t *testing.T) { UserChannelAccess: []string{username}, }) rt1, rt2 := peers.ActiveRT, peers.PassiveRT - docID := rest.SafeDocumentName(t, t.Name()+"rt2doc1") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt2doc1") version := rt2.PutDoc(docID, `{"source":"rt2","channels":["alice"]}`) ctx1 := rt1.Context() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePull, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -3972,7 +3973,7 @@ func TestActiveReplicatorPullConflict(t *testing.T) { replicationStats := dbReplicatorStats(t, rt1.GetDatabase()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePull, RemoteDBURL: remoteURL, ActiveDB: &db.Database{ @@ -4209,7 +4210,7 @@ func TestActiveReplicatorPushAndPullConflict(t *testing.T) { customConflictResolver, err := db.NewCustomConflictResolver(ctx1, test.conflictResolver, rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, @@ -4348,11 +4349,11 @@ func TestActiveReplicatorPushBasicWithInsecureSkipVerifyEnabled(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()+"rt1doc1") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt1doc1") version := rt1.PutDoc(docID, `{"source":"rt1","channels":["alice"]}`) rt1.WaitForPendingChanges() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) // Make rt2 listen on an actual HTTP port, so it can receive the blipsync request from rt1. srv := httptest.NewTLSServer(rt2.TestPublicHandler()) @@ -4414,7 +4415,7 @@ func TestActiveReplicatorPushBasicWithInsecureSkipVerifyDisabled(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()+"rt1doc1") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt1doc1") resp := rt1.SendAdminRequest(http.MethodPut, "/{{.keyspace}}/"+docID, `{"source":"rt1","channels":["alice"]}`) rest.RequireStatus(t, resp, http.StatusCreated) @@ -4428,7 +4429,7 @@ func TestActiveReplicatorPushBasicWithInsecureSkipVerifyDisabled(t *testing.T) { // Add basic auth creds to target db URL passiveDBURL.User = url.UserPassword(username, rest.RestTesterDefaultUserPassword) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, @@ -4478,7 +4479,7 @@ func TestActiveReplicatorRecoverFromLocalFlush(t *testing.T) { rt2.CreateUser(username, []string{username}) // Create doc on rt2 - docID := rest.SafeDocumentName(t, t.Name()) + "rt2doc" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt2doc" resp := rt2.SendAdminRequest(http.MethodPut, "/{{.keyspace}}/"+docID, `{"source":"rt2","channels":["alice"]}`) rest.RequireStatus(t, resp, http.StatusCreated) @@ -4497,7 +4498,7 @@ func TestActiveReplicatorRecoverFromLocalFlush(t *testing.T) { rt1 := rest.NewRestTester(t, nil) ctx1 := rt1.Context() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) arConfig := db.ActiveReplicatorConfig{ ID: replicationID, @@ -4646,14 +4647,14 @@ func TestActiveReplicatorRecoverFromRemoteFlush(t *testing.T) { ctx1 := rt1.Context() // Create doc on rt1 - docID := rest.SafeDocumentName(t, t.Name()+"rt1doc") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt1doc") resp := rt1.SendAdminRequest(http.MethodPut, "/{{.keyspace}}/"+docID, `{"source":"rt1","channels":["alice"]}`) rest.RequireStatus(t, resp, http.StatusCreated) rt1.WaitForPendingChanges() arConfig := db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: passiveDBURL, ActiveDB: &db.Database{ @@ -4794,13 +4795,13 @@ func TestActiveReplicatorRecoverFromRemoteRollback(t *testing.T) { ctx1 := activeRT.Context() // Create doc1 on activeRT - docID := rest.SafeDocumentName(t, t.Name()+"rt1doc") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt1doc") docID2 := docID + "2" resp := activeRT.SendAdminRequest(http.MethodPut, "/{{.keyspace}}/"+docID, `{"source":"activeRT","channels":["alice"]}`) rest.RequireStatus(t, resp, http.StatusCreated) activeRT.WaitForPendingChanges() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) arConfig := db.ActiveReplicatorConfig{ ID: replicationID, @@ -4937,7 +4938,7 @@ func TestActiveReplicatorRecoverFromMismatchedRev(t *testing.T) { require.NoError(t, err) ctx1 := rt1.Context() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) arConfig := db.ActiveReplicatorConfig{ ID: replicationID, @@ -4974,7 +4975,7 @@ func TestActiveReplicatorRecoverFromMismatchedRev(t *testing.T) { require.NoError(t, err) // Create doc1 on rt1 - docID := rest.SafeDocumentName(t, t.Name()+"rt1doc") + docID := sgtest.SafeDocumentName(t, t.Name()+"rt1doc") resp := rt1.SendAdminRequest(http.MethodPut, "/{{.keyspace}}/"+docID, `{"source":"rt1","channels":["alice"]}`) rest.RequireStatus(t, resp, http.StatusCreated) rt1.WaitForPendingChanges() @@ -4984,7 +4985,7 @@ func TestActiveReplicatorRecoverFromMismatchedRev(t *testing.T) { assert.Equal(t, docID, changesResults.Results[0].ID) // Create doc2 on rt2 - docID = rest.SafeDocumentName(t, t.Name()+"rt2doc") + docID = sgtest.SafeDocumentName(t, t.Name()+"rt2doc") resp = rt2.SendAdminRequest(http.MethodPut, "/{{.keyspace}}/"+docID, `{"source":"rt2","channels":["alice"]}`) rest.RequireStatus(t, resp, http.StatusCreated) rt2.WaitForPendingChanges() @@ -5025,10 +5026,10 @@ func TestActiveReplicatorIgnoreNoConflicts(t *testing.T) { require.NoError(t, err) ctx1 := rt1.Context() - rt1docID := rest.SafeDocumentName(t, t.Name()) + "rt1doc1" + rt1docID := sgtest.SafeDocumentName(t, t.Name()) + "rt1doc1" rt1Version := rt1.PutDoc(rt1docID, `{"source":"rt1","channels":["alice"]}`) - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: replicationID, @@ -5062,7 +5063,7 @@ func TestActiveReplicatorIgnoreNoConflicts(t *testing.T) { assert.Equal(t, "rt1", body["source"]) // write a doc on rt2 ... - rt2docID := rest.SafeDocumentName(t, t.Name()+"rt2doc1") + rt2docID := sgtest.SafeDocumentName(t, t.Name()+"rt2doc1") rt2Version := rt2.PutDoc(rt2docID, `{"source":"rt2","channels":["alice"]}`) // ... and wait to arrive at rt1 @@ -5111,14 +5112,14 @@ func TestActiveReplicatorPullModifiedHash(t *testing.T) { require.NoError(t, err) // Create first batch of docs, creating numRT2DocsInitial in each channel - docIDPrefix := rest.SafeDocumentName(t, t.Name()+"rt2doc") + docIDPrefix := sgtest.SafeDocumentName(t, t.Name()+"rt2doc") for i := range numDocsPerChannelInitial { rt2.PutDoc(fmt.Sprintf("%s_%s_%d", docIDPrefix, "chan1", i), `{"source":"rt2","channels":["chan1"]}`) rt2.PutDoc(fmt.Sprintf("%s_%s_%d", docIDPrefix, "chan2", i), `{"source":"rt2","channels":["chan2"]}`) } ctx1 := rt1.Context() - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) arConfig := db.ActiveReplicatorConfig{ ID: replicationID, @@ -5713,7 +5714,7 @@ func TestActiveReplicatorPullConflictReadWriteIntlProps(t *testing.T) { rt1Version := rt1.PutNewEditsFalse(docID, test.localVersion, test.commonAncestorVersion, test.localRevisionBody) rest.RequireDocRevTreeEqual(t, test.localVersion, *rt1Version) - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) customConflictResolver, err := db.NewCustomConflictResolver(ctx1, test.conflictResolver, rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) replicationStats := rest.DbReplicatorStats(t, rt1.GetDatabase(), id) @@ -6563,7 +6564,7 @@ func TestReplicatorConflictAttachment(t *testing.T) { parentVersion = activeRT.PutNewEditsFalse(docID, newVersion, parentVersion, "{}") } - replicationID := "replication_" + rest.SafeDocumentName(t, t.Name()) + replicationID := "replication_" + sgtest.SafeDocumentName(t, t.Name()) activeRT.CreateReplication(replicationID, passiveDBURL, db.ActiveReplicatorTypePushAndPull, nil, true, test.conflictResolution, "") defer activeRT.DeleteReplication(replicationID) activeRT.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning) @@ -6649,7 +6650,7 @@ func TestConflictResolveMergeWithMutatedRev(t *testing.T) { }`, rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) replicationStats := rest.DbReplicatorStats(t, rt1.GetDatabase(), id) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ @@ -6704,7 +6705,7 @@ func TestReplicatorDoNotSendDeltaWhenSrcIsTombstone(t *testing.T) { activeRT.WaitForVersion("test", version) // Set-up replicator // - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) replicationStats := rest.DbReplicatorStats(t, activeRT.GetDatabase(), id) ar, err := db.NewActiveReplicator(activeCtx, &db.ActiveReplicatorConfig{ @@ -6782,7 +6783,7 @@ func TestUnprocessableDeltas(t *testing.T) { version := activeRT.PutDoc("test", `{"field1":"f1_1","field2":"f2_1"}`) activeRT.WaitForVersion("test", version) - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(activeCtx, &db.ActiveReplicatorConfig{ ID: id, Direction: db.ActiveReplicatorTypePush, @@ -6846,7 +6847,7 @@ func TestReplicatorIgnoreRemovalBodies(t *testing.T) { activeCtx := activeRT.Context() collection, _ := activeRT.GetSingleTestDatabaseCollection() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) // Create the docs // // Doc rev 1 version1 := activeRT.PutDoc(docID, `{"key":"12","channels": ["rev1chan"]}`) @@ -6864,7 +6865,7 @@ func TestReplicatorIgnoreRemovalBodies(t *testing.T) { err := collection.PurgeOldRevisionJSON(activeCtx, docID, version2.RevTreeID) require.NoError(t, err) - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(activeCtx, &db.ActiveReplicatorConfig{ ID: id, Direction: db.ActiveReplicatorTypePush, @@ -6904,12 +6905,12 @@ func TestUnderscorePrefixSupport(t *testing.T) { activeCtx := activeRT.Context() // Create the document - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) rawDoc := `{"_foo": true, "_exp": 120, "true": false, "_attachments": {"bar": {"data": "Zm9vYmFy"}}}` _ = activeRT.PutDoc(docID, rawDoc) // Set-up replicator - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(activeCtx, &db.ActiveReplicatorConfig{ ID: id, Direction: db.ActiveReplicatorTypePush, @@ -6995,7 +6996,7 @@ func TestActiveReplicatorBlipsync(t *testing.T) { require.NoError(t, err) ctx := rt.Context() - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx, &db.ActiveReplicatorConfig{ ID: id, Direction: db.ActiveReplicatorTypePushAndPull, @@ -7141,7 +7142,7 @@ func TestReplicatorCheckpointOnStop(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) activeRT, passiveRT, remoteURL := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL - replicationID := rest.SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) // increase checkpointing interval temporarily to ensure the checkpointer doesn't fire on an // interval during the running of the test @@ -7977,7 +7978,7 @@ func TestISGRRunAsNonExistentUserPushesAsAdmin(t *testing.T) { // Admin-authored private document on active, in a channel that would // never match a scoped run_as user's access (because no such user // exists to grant access to). - docID := "private_doc_" + rest.SafeDocumentName(t, t.Name()) + docID := "private_doc_" + sgtest.SafeDocumentName(t, t.Name()) version := activeRT.PutDoc(docID, `{"channels":["`+privateChannel+`"],"content":"secret"}`) activeRT.WaitForPendingChanges() @@ -8116,7 +8117,7 @@ func TestISGRRunAsNonExistentUserReplicationConfigInDbConfig(t *testing.T) { // Admin-authored private document; a non-existent run_as user can never // be granted access to this channel. - docID := "private_doc_" + rest.SafeDocumentName(t, t.Name()) + docID := "private_doc_" + sgtest.SafeDocumentName(t, t.Name()) activeRT.PutDoc(docID, `{"channels":["`+privateChannel+`"],"content":"secret"}`) activeRT.WaitForPendingChanges() @@ -8149,7 +8150,7 @@ func TestActiveReplicatorPullNewDocChannels(t *testing.T) { ctx1 := activeRT.Context() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePull, RemoteDBURL: remoteURL, ActiveDB: &db.Database{ @@ -8166,7 +8167,7 @@ func TestActiveReplicatorPullNewDocChannels(t *testing.T) { require.NoError(t, ar.Start(ctx1)) // brand new document created on the remote, assigned to chanName by the sync function - docID := rest.SafeDocumentName(t, t.Name()) + "rt2doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt2doc1" version := passiveRT.PutDoc(docID, `{"source":"rt2","channels":["`+chanName+`"]}`) sgrRunner.WaitForDocReplicated(docID, passiveRT, activeRT, version) @@ -8204,7 +8205,7 @@ func TestActiveReplicatorPushNewDocChannels(t *testing.T) { ctx1 := activeRT.Context() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: userDBURL(passiveRT, username), ActiveDB: &db.Database{ @@ -8221,7 +8222,7 @@ func TestActiveReplicatorPushNewDocChannels(t *testing.T) { require.NoError(t, ar.Start(ctx1)) // brand new document created on the active peer, assigned to chanName by the sync function - docID := rest.SafeDocumentName(t, t.Name()) + "rt1doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "rt1doc1" version := activeRT.PutDoc(docID, `{"source":"rt1","channels":["`+chanName+`"]}`) sgrRunner.WaitForDocReplicated(docID, activeRT, passiveRT, version) @@ -8261,7 +8262,7 @@ func TestActiveReplicatorPullUpdatedDocChannels(t *testing.T) { ctx1 := activeRT.Context() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePushAndPull, RemoteDBURL: userDBURL(passiveRT, username), ActiveDB: &db.Database{ @@ -8278,7 +8279,7 @@ func TestActiveReplicatorPullUpdatedDocChannels(t *testing.T) { require.NoError(t, ar.Start(ctx1)) // doc originates LOCALLY on active, so active's channel metadata starts out correct - docID := rest.SafeDocumentName(t, t.Name()) + "doc1" + docID := sgtest.SafeDocumentName(t, t.Name()) + "doc1" v1 := activeRT.PutDoc(docID, `{"channels":["`+chanA+`"]}`) activeRTCollection, activeRTCtx := activeRT.GetSingleTestDatabaseCollection() localDoc, err := activeRTCollection.GetDocument(activeRTCtx, docID, db.DocUnmarshalAll) @@ -8358,7 +8359,7 @@ func TestISGRInvalidRevTreeRepairedBeforeSend(t *testing.T) { sgrRunner.Run(func(t *testing.T) { peers := sgrRunner.SetupSGRPeers(t) activeRT, passiveRT, passiveDBURL := peers.ActiveRT, peers.PassiveRT, peers.PassiveDBURL - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) activeCollection, activeCollectionCtx := activeRT.GetSingleTestDatabaseCollectionWithUser() db.PlantRevTreeForTest(t, activeCollectionCtx, activeCollection, docID, @@ -8374,8 +8375,8 @@ func TestISGRInvalidRevTreeRepairedBeforeSend(t *testing.T) { require.Equal(t, quietCaseTree, tree) require.Equal(t, int64(0), invalidRevTreeCount.Value()) - activeRT.CreateReplication(rest.SafeDocumentName(t, t.Name()), passiveDBURL, db.ActiveReplicatorTypePush, nil, true, db.ConflictResolverDefault, "") - activeRT.WaitForReplicationStatus(rest.SafeDocumentName(t, t.Name()), db.ReplicationStateRunning) + activeRT.CreateReplication(sgtest.SafeDocumentName(t, t.Name()), passiveDBURL, db.ActiveReplicatorTypePush, nil, true, db.ConflictResolverDefault, "") + activeRT.WaitForReplicationStatus(sgtest.SafeDocumentName(t, t.Name()), db.ReplicationStateRunning) // passive get repaired version of doc passiveCollection, passiveCollectionCtx := passiveRT.GetSingleTestDatabaseCollectionWithUser() diff --git a/rest/replicatortest/replicator_test_legacy_rev_test.go b/rest/replicatortest/replicator_test_legacy_rev_test.go index bd325259ab..8ffeba2e3c 100644 --- a/rest/replicatortest/replicator_test_legacy_rev_test.go +++ b/rest/replicatortest/replicator_test_legacy_rev_test.go @@ -18,6 +18,7 @@ import ( "github.com/couchbase/sync_gateway/rest" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) func TestActiveReplicatorPushPullLegacyRev(t *testing.T) { @@ -34,12 +35,12 @@ func TestActiveReplicatorPushPullLegacyRev(t *testing.T) { }) rt1, rt2 := peers.ActiveRT, peers.PassiveRT - docIDRT2 := rest.SafeDocumentName(t, t.Name()+"rt2doc1") + docIDRT2 := sgtest.SafeDocumentName(t, t.Name()+"rt2doc1") rt2InitDoc := rt2.CreateDocNoHLV(docIDRT2, db.Body{"source": "rt2", "channels": []string{username}}) legacyRevRt2 := rt2InitDoc.GetRevTreeID() ctx1 := rt1.Context() - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) docIDRT1 := id + "rt1doc1" rt1InitDoc := rt1.CreateDocNoHLV(docIDRT1, db.Body{"source": "rt1", "channels": []string{username}}) legacyRevRt1 := rt1InitDoc.GetRevTreeID() @@ -119,7 +120,7 @@ func TestActiveReplicatorBiDirectionalPreUpgradedDocOnPeer(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) var legacyRevRT1, legacyRevRT2, initLegacyRevRT1, initLegacyRevRT2 string @@ -151,7 +152,7 @@ func TestActiveReplicatorBiDirectionalPreUpgradedDocOnPeer(t *testing.T) { legacyRevRT2 = rt2DocInit.GetRevTreeID() } - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: id, Direction: db.ActiveReplicatorTypePushAndPull, @@ -227,7 +228,7 @@ func TestActiveReplicatorBiDirectionalPreUpgradedDocOnBothSidesAlreadyKnownRev(t rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) // create doc on rt2 with two revisions bodyRT2 := db.Body{"channels": []string{username}} @@ -344,7 +345,7 @@ func TestActiveReplicatorBiDirectionalPreUpgradedRevInHistory(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) var initLegacyRevRT2, legacyRevRT2, upgradedRevID, legacyRevRT1, initLegacyRevRT1 string var upgradedDocVersion rest.DocVersion @@ -485,8 +486,8 @@ func TestActiveReplicatorPushPullNewDocLegacyRevAndAllowUpdateAfter(t *testing.T rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docIDToPush := rest.SafeDocumentName(t, t.Name()+"_push") - docIDToPull := rest.SafeDocumentName(t, t.Name()+"_pull") + docIDToPush := sgtest.SafeDocumentName(t, t.Name()+"_push") + docIDToPull := sgtest.SafeDocumentName(t, t.Name()+"_pull") // create doc on rt1 with one revision bodyRT1 := db.Body{"channels": []string{username}, "source": "rt1"} @@ -498,7 +499,7 @@ func TestActiveReplicatorPushPullNewDocLegacyRevAndAllowUpdateAfter(t *testing.T rt2InitDoc := rt2.CreateDocNoHLV(docIDToPull, bodyRT2) legacyRevRt2 := rt2InitDoc.GetRevTreeID() - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) replicationStats := rest.DbReplicatorStats(t, rt1.GetDatabase(), id) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ @@ -581,7 +582,7 @@ func TestActiveReplicatorPushConflictingPreUpgradedVersion(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) // create doc on rt1 with two revisions bodyRT1 := db.Body{"channels": []string{username}} @@ -600,7 +601,7 @@ func TestActiveReplicatorPushConflictingPreUpgradedVersion(t *testing.T) { legacyRevRT2 := rt2InitDoc.GetRevTreeID() ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ - ID: rest.SafeDocumentName(t, t.Name()), + ID: sgtest.SafeDocumentName(t, t.Name()), Direction: db.ActiveReplicatorTypePush, RemoteDBURL: userDBURL(rt2, username), ActiveDB: &db.Database{ @@ -691,7 +692,7 @@ func TestActiveReplicatorConflictPreUpgradedVersionEachSide(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) var legacyRevRT1, initLegacyRevRT1, legacyRevRT2, initLegacyRevRT2 string if tc.activeWins { @@ -734,7 +735,7 @@ func TestActiveReplicatorConflictPreUpgradedVersionEachSide(t *testing.T) { resolverFuncRevID, err := db.NewConflictResolverFunc(ctx1, db.ConflictResolverDefault, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) replicationStats := rest.DbReplicatorStats(t, rt1.GetDatabase(), id) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ @@ -863,7 +864,7 @@ func TestActiveReplicatorConflictPreUpgradedVersionOneSide(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docID := rest.SafeDocumentName(t, t.Name()) + docID := sgtest.SafeDocumentName(t, t.Name()) var legacyRevRT1, initLegacyRevRT1, legacyRevRT2, initLegacyRevRT2, expectedBody string var upgradedDocVersion rest.DocVersion @@ -909,7 +910,7 @@ func TestActiveReplicatorConflictPreUpgradedVersionOneSide(t *testing.T) { resolverFuncRevID, err := db.NewConflictResolverFunc(ctx1, db.ConflictResolverDefault, "", rt1.GetDatabase().Options.JavascriptTimeout) require.NoError(t, err) - id := rest.SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) replicationStats := rest.DbReplicatorStats(t, rt1.GetDatabase(), id) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ @@ -1031,7 +1032,7 @@ func TestActiveReplicatorDeltaSyncWhenBothSidesLegacy(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docIDToPush := rest.SafeDocumentName(t, t.Name()+"_push") + docIDToPush := sgtest.SafeDocumentName(t, t.Name()+"_push") // create doc on rt1 with one revision bodyRT1 := db.Body{"channels": []string{username}, "source": "rt1"} @@ -1101,7 +1102,7 @@ func TestDeltaSyncWhenOneSideHasEncodedCV(t *testing.T) { rt1, rt2 := peers.ActiveRT, peers.PassiveRT ctx1 := rt1.Context() - docIDToPush := rest.SafeDocumentName(t, t.Name()+"_push") + docIDToPush := sgtest.SafeDocumentName(t, t.Name()+"_push") // create doc on rt1 with one revision bodyRT1 := db.Body{"channels": []string{username}, "source": "rt1"} diff --git a/rest/revocation_test.go b/rest/revocation_test.go index e105dfbe06..f5ae67cb93 100644 --- a/rest/revocation_test.go +++ b/rest/revocation_test.go @@ -25,6 +25,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) type ChannelRevocationTester struct { @@ -1530,7 +1531,7 @@ func TestReplicatorRevocations(t *testing.T) { require.NoError(t, err) passiveDBURL.User = url.UserPassword("user", RestTesterDefaultUserPassword) - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: id, @@ -1597,7 +1598,7 @@ func TestReplicatorRevocationsNoRev(t *testing.T) { require.NoError(t, err) passiveDBURL.User = url.UserPassword("user", RestTesterDefaultUserPassword) - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: id, @@ -1673,7 +1674,7 @@ func TestReplicatorRevocationsNoRevButAlternateAccess(t *testing.T) { require.NoError(t, err) passiveDBURL.User = url.UserPassword("user", RestTesterDefaultUserPassword) - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: id, @@ -1744,7 +1745,7 @@ func TestReplicatorRevocationsMultipleAlternateAccess(t *testing.T) { require.NoError(t, err) passiveDBURL.User = url.UserPassword(revocationTestUser, RestTesterDefaultUserPassword) - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) revocationTester.addRole(revocationTestUser, revocationTestRole) rt2.WaitForPendingChanges() @@ -1843,7 +1844,7 @@ func TestReplicatorRevocationsWithTombstoneResurrection(t *testing.T) { require.NoError(t, err) passiveDBURL.User = url.UserPassword("user", "letmein") - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{ ID: id, @@ -1925,7 +1926,7 @@ func TestReplicatorRevocationsWithChannelFilter(t *testing.T) { password = RestTesterDefaultUserPassword ) - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) passiveDBURL.User = url.UserPassword(username, password) _ = rt2.PutDoc("docA", `{"channels": ["ABC"]}`) @@ -2002,7 +2003,7 @@ func TestReplicatorRevocationsWithStarChannel(t *testing.T) { require.NoError(t, err) passiveDBURL.User = url.UserPassword("user", RestTesterDefaultUserPassword) - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) _ = rt2.PutDoc("docA", `{"channels": ["A"]}`) _ = rt2.PutDoc("docAB", `{"channels": ["A","B"]}`) @@ -2090,7 +2091,7 @@ func TestReplicatorRevocationsFromZero(t *testing.T) { require.NoError(t, err) passiveDBURL.User = url.UserPassword("user", "letmein") - id := SafeDocumentName(t, t.Name()) + id := sgtest.SafeDocumentName(t, t.Name()) activeReplCfg := &db.ActiveReplicatorConfig{ ID: id, @@ -2465,7 +2466,7 @@ func TestReplicatorSwitchPurgeNoReset(t *testing.T) { peers := sgrRunner.SetupSGRPeersWithOptions(t, TestISGRPeerOpts{UserChannelAccess: []string{"A", "B"}}) rt1, rt2 := peers.ActiveRT, peers.PassiveRT - replicationID := SafeDocumentName(t, t.Name()) + replicationID := sgtest.SafeDocumentName(t, t.Name()) for i := range 10 { _ = rt2.PutDoc(fmt.Sprintf("docA%d", i), `{"channels": ["A"]}`) diff --git a/rest/user_api_test.go b/rest/user_api_test.go index d46469a508..ed45b203bc 100644 --- a/rest/user_api_test.go +++ b/rest/user_api_test.go @@ -27,6 +27,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // TestUsersAPI tests pagination of QueryPrincipals @@ -1420,7 +1421,7 @@ func TestPutUserCollectionAccess(t *testing.T) { for _, tc := range collectionAccessTestCases { t.Run(tc.name, func(t *testing.T) { // Create a fresh user and role for this test case, each with initial access to collection1. - userName := "user" + SafeDocumentName(t, tc.name) + userName := "user" + sgtest.SafeDocumentName(t, tc.name) userPayload := fmt.Sprintf(`{ "email": "%s@couchbase.com", "password": "letmein", @@ -1434,7 +1435,7 @@ func TestPutUserCollectionAccess(t *testing.T) { }`, userName, scopeName, collection1Name) RequireStatus(t, rt.SendAdminRequest("PUT", "/db/_user/"+userName, userPayload), http.StatusCreated) - roleName := "role" + SafeDocumentName(t, tc.name) + roleName := "role" + sgtest.SafeDocumentName(t, tc.name) rolePayload := fmt.Sprintf(`{ "collection_access": { "%s": { diff --git a/rest/utilities_testing.go b/rest/utilities_testing.go index 0bcdb31e30..10f53f7927 100644 --- a/rest/utilities_testing.go +++ b/rest/utilities_testing.go @@ -2643,17 +2643,6 @@ func SafeDatabaseName(t *testing.T, name string) string { return dbName } -// SafeDocumentName returns a document name free of any special characters for use in tests. -func SafeDocumentName(t *testing.T, name string) string { - t.Helper() - docName := strings.ToLower(name) - for _, c := range []string{" ", "<", ">", "/", "="} { - docName = strings.ReplaceAll(docName, c, "_") - } - require.Less(t, len(docName), 251, "Document name %s is too long, must be less than 251 characters", name) - return docName -} - func JsonToMap(t *testing.T, jsonStr string) map[string]any { result := make(map[string]any) err := json.Unmarshal([]byte(jsonStr), &result) diff --git a/rest/utilities_testing_blip_client_test.go b/rest/utilities_testing_blip_client_test.go index 8e90182faf..108781a379 100644 --- a/rest/utilities_testing_blip_client_test.go +++ b/rest/utilities_testing_blip_client_test.go @@ -22,6 +22,7 @@ import ( "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/testing/assert" "github.com/couchbase/sync_gateway/testing/require" + "github.com/couchbase/sync_gateway/testing/sgtest" ) // newTestPullMessage builds an incoming message in the shape Sync Gateway sends it, so a test can drive @@ -62,7 +63,7 @@ func TestBlipTesterClientPullHandling(t *testing.T) { // placeholder carries the cv but no body, so the rev that follows is the first copy of the document the // client holds, and must be stored rather than answered as a revision the client already has. func requireRevAfterNoRevIsStored(t *testing.T, ctx context.Context, client *BlipTesterClient, btcc *BlipTesterCollectionClient) { - docID := SafeDocumentName(t, t.Name()) + "_revAfterNoRev" + docID := sgtest.SafeDocumentName(t, t.Name()) + "_revAfterNoRev" hlv := db.NewHybridLogicalVector() require.NoError(t, hlv.AddVersion(db.Version{SourceID: "sourceA", Value: 100})) version := DocVersion{CV: *hlv.ExtractCurrentVersionFromHLV()} @@ -97,7 +98,7 @@ func requireDuplicateRevIsNoop(t *testing.T, ctx context.Context, client *BlipTe return btcc._seqLast } - docID := SafeDocumentName(t, t.Name()) + "_duplicateRev" + docID := sgtest.SafeDocumentName(t, t.Name()) + "_duplicateRev" hlv := db.NewHybridLogicalVector() require.NoError(t, hlv.AddVersion(db.Version{SourceID: "sourceB", Value: 200})) version := DocVersion{CV: *hlv.ExtractCurrentVersionFromHLV()} @@ -147,7 +148,7 @@ func requireReplacedRevIsStored(t *testing.T, ctx context.Context, client *BlipT handleRev := client.pullReplication.handleRev(ctx, client) for _, deleted := range []bool{false, true} { - docID := fmt.Sprintf("%s_replacedRev_deleted_%t", SafeDocumentName(t, t.Name()), deleted) + docID := fmt.Sprintf("%s_replacedRev_deleted_%t", sgtest.SafeDocumentName(t, t.Name()), deleted) properties := blip.Properties{ db.RevMessageID: docID, db.RevMessageRev: sentRev, diff --git a/testing/sgtest/sgtest.go b/testing/sgtest/sgtest.go index fc8b1abe3f..d36c08c443 100644 --- a/testing/sgtest/sgtest.go +++ b/testing/sgtest/sgtest.go @@ -16,6 +16,7 @@ import ( "testing" "time" + "github.com/couchbase/sync_gateway/testing/require" rosmar "github.com/couchbaselabs/rosmar" ) @@ -59,3 +60,14 @@ func GetBackgroundManagerStatusTransitionTimeout(t testing.TB) time.Duration { } return 10 * time.Second } + +// SafeDocumentName returns a document name free of any special characters banned by Couchbase Server for use in tests. +func SafeDocumentName(t *testing.T, name string) string { + t.Helper() + docName := strings.ToLower(name) + for _, c := range []string{" ", "<", ">", "/", "="} { + docName = strings.ReplaceAll(docName, c, "_") + } + require.Less(t, len(docName), 251, "Document name %s is too long, must be less than 251 characters", name) + return docName +}