-
Notifications
You must be signed in to change notification settings - Fork 140
CBG-3689: CheckProposedRev does not unmarshall the entire history #7875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2282,3 +2282,97 @@ | |||||
| }) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestProposedRev(t *testing.T) { | ||||||
|
|
||||||
| base.SetUpTestLogging(t, base.LevelDebug, base.KeyAll) | ||||||
|
||||||
|
|
||||||
| db, ctx := setupTestDB(t) | ||||||
| defer db.Close(ctx) | ||||||
| collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) | ||||||
|
|
||||||
| // create 3 documents | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: To make this easier to read and debug in the future I think I would do something like name these documents with what they are: const ( |
||||||
| body := Body{"key1": "value1", "key2": 1234} | ||||||
| _, doc1, err := collection.Put(ctx, "doc1", body) | ||||||
| require.NoError(t, err) | ||||||
| doc1Rev := doc1.GetRevTreeID() | ||||||
|
|
||||||
| _, doc2, err := collection.Put(ctx, "doc2", body) | ||||||
| require.NoError(t, err) | ||||||
| doc2Rev1 := doc2.GetRevTreeID() | ||||||
| _, doc2, err = collection.Put(ctx, "doc2", Body{"_rev": doc2Rev1, "key1": "value2", "key2": 5678}) | ||||||
| require.NoError(t, err) | ||||||
| doc2Rev2 := doc2.GetRevTreeID() | ||||||
|
|
||||||
| _, doc3, err := collection.Put(ctx, "doc3", body) | ||||||
| require.NoError(t, err) | ||||||
| doc3Rev1 := doc3.GetRevTreeID() | ||||||
| _, doc3, err = collection.Put(ctx, "doc3", Body{"_rev": doc3Rev1, "_deleted": true}) | ||||||
|
Check failure on line 2310 in db/crud_test.go
|
||||||
| require.NoError(t, err) | ||||||
|
|
||||||
| testCases := []struct { | ||||||
| name string | ||||||
| revID string | ||||||
| parentRevID string | ||||||
| expectedStatus ProposedRevStatus | ||||||
| currentRev string | ||||||
|
||||||
| currentRev string | |
| expectedCurrentRev string |
This indicates it is the return value of the struct
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General comment, but I think this is worth addressing for this PR.
When writing a test, you want to think about the way the code is called, not just the implementation. Some of the idea of testing is to cover the cases that don't have different code pathways today but do have different code pathways in the future.
So there are several to account for and in a fast test it would be great to hit all of the possibilities
- type of document: (no document, multiple rev, tombstone)
- arguments (revID, parentRevID)
- revID possibilities:
- revID doesn't exist
- revID does exist and is current rev
- revID does exist and is in history
- parentRevID
- parentRevID doesn't exist
- parentRevID does exist, and is the immediate parent
- parentRevID does exist in the history but isn't the immediate parent. (In this case, you can test this by having the documents have three revisions so you can test for a past parent)
I'm not going to list all these permutations, but given how fast this test runs I think it would be fine to hit most of them. I think this is 3 * 3 * 3 which is a manageable number. If it was too many, then I think consider skip more, but this gives more coverage to this function. If this does really blow up because I did the math wrong, we definitely can consider shortening the number of cases.
the first example that strikes me is what happens when you have:
- no existing document
- rev: 2-def
- parentRev: 1-def
That's a common scenario where Couchbase Lite is pushing a document that has two local revisions and Sync Gateway has none.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,13 +37,14 @@ | |||||||
| type DocumentUnmarshalLevel uint8 | ||||||||
|
|
||||||||
| const ( | ||||||||
| DocUnmarshalAll = DocumentUnmarshalLevel(iota) // Unmarshals metadata and body | ||||||||
|
Check failure on line 40 in db/document.go
|
||||||||
| DocUnmarshalSync // Unmarshals metadata | ||||||||
| DocUnmarshalNoHistory // Unmarshals metadata excluding revtree history | ||||||||
| DocUnmarshalHistory // Unmarshals revtree history + rev + CAS only | ||||||||
| DocUnmarshalRev // Unmarshals revTreeID + CAS only (no HLV) | ||||||||
| DocUnmarshalCAS // Unmarshals CAS (for import check) only | ||||||||
| DocUnmarshalNone // No unmarshalling (skips import/upgrade check) | ||||||||
| DocUnmarshalRevAndFlags // Unmarshals revTreeID + CAS and Flags (no HLV) | ||||||||
| ) | ||||||||
|
|
||||||||
| const ( | ||||||||
|
|
@@ -167,6 +168,14 @@ | |||||||
| sd.RevAndVersion.CurrentVersion = string(base.Uint64CASToLittleEndianHex(hlv.Version)) | ||||||||
| } | ||||||||
|
|
||||||||
| func (sd *SyncData) hasFlag(flag uint8) bool { | ||||||||
torcolvin marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| return sd.Flags&flag != 0 | ||||||||
| } | ||||||||
|
|
||||||||
| func (sd *SyncData) IsDeleted() bool { | ||||||||
torcolvin marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| return sd.hasFlag(channels.Deleted) | ||||||||
| } | ||||||||
|
|
||||||||
| // RedactRawGlobalSyncData runs HashRedact on the given global sync data. | ||||||||
| func RedactRawGlobalSyncData(syncData []byte, redactSalt string) ([]byte, error) { | ||||||||
| if redactSalt == "" { | ||||||||
|
|
@@ -306,6 +315,11 @@ | |||||||
| CurrentRev channels.RevAndVersion `json:"rev"` | ||||||||
| } | ||||||||
|
|
||||||||
| type revAndFlagsSyncData struct { | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| revOnlySyncData | ||||||||
| Flags uint8 `json:"flags"` | ||||||||
| } | ||||||||
|
|
||||||||
| type casOnlySyncData struct { | ||||||||
| Cas string `json:"cas"` | ||||||||
| } | ||||||||
|
|
@@ -331,10 +345,6 @@ | |||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| func (doc *Document) IsDeleted() bool { | ||||||||
| return doc.hasFlag(channels.Deleted) | ||||||||
| } | ||||||||
|
|
||||||||
| func (doc *Document) BodyWithSpecialProperties(ctx context.Context) ([]byte, error) { | ||||||||
| bodyBytes, err := doc.BodyBytes(ctx) | ||||||||
| if err != nil { | ||||||||
|
|
@@ -754,10 +764,6 @@ | |||||||
| return true, true, false | ||||||||
| } | ||||||||
|
|
||||||||
| func (doc *Document) hasFlag(flag uint8) bool { | ||||||||
| return doc.Flags&flag != 0 | ||||||||
| } | ||||||||
|
|
||||||||
| func (doc *Document) setFlag(flag uint8, state bool) { | ||||||||
| if state { | ||||||||
| doc.Flags |= flag | ||||||||
|
|
@@ -1336,6 +1342,23 @@ | |||||||
| doc.SyncData = SyncData{} | ||||||||
| } | ||||||||
| doc._rawBody = data | ||||||||
| case DocUnmarshalRevAndFlags: | ||||||||
| // Unmarshal rev ,cas and flags from sync metadata | ||||||||
|
||||||||
| // Unmarshal rev ,cas and flags from sync metadata | |
| // Unmarshal rev, cas and flags from sync metadata |
Outdated
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error message references 'DocUnmarshalRev' but this case is for 'DocUnmarshalRevAndFlags'. Update the error message to accurately reflect the unmarshal level being used.
| return pkgerrors.WithStack(base.RedactErrorf("Failed to UnmarshalWithXattrs() doc with id: %s (DocUnmarshalRev). Error: %v", base.UD(doc.ID), unmarshalErr)) | |
| return pkgerrors.WithStack(base.RedactErrorf("Failed to UnmarshalWithXattrs() doc with id: %s (DocUnmarshalRevAndFlags). Error: %v", base.UD(doc.ID), unmarshalErr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
hasFlagmethod is not exported and is being called onSyncData. Based on the code changes,IsDeleted()method was added toSyncDatawhich should be used instead for better encapsulation and consistency.