diff --git a/examples/example_test.go b/examples/example_test.go index 2fa8525..e8e0179 100644 --- a/examples/example_test.go +++ b/examples/example_test.go @@ -1,7 +1,7 @@ package examples import ( - dynamock "github.com/gusaul/go-dynamock" + "github.com/caldwecr/go-dynamock" "testing" "github.com/aws/aws-sdk-go/aws" diff --git a/go.mod b/go.mod index 1c928b2..5e15f78 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/gusaul/go-dynamock +module github.com/caldwecr/go-dynamock go 1.15 diff --git a/go.sum b/go.sum index cdd1f24..22e9060 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,28 @@ github.com/aws/aws-sdk-go v1.36.22 h1:kkQdiotYI9RlGoAoMPbQyHKsl9oyT+vz/w2cN6EUZKs= github.com/aws/aws-sdk-go v1.36.22/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/types.go b/types.go index f898888..8847e84 100644 --- a/types.go +++ b/types.go @@ -43,10 +43,15 @@ type ( // UpdateItemExpectation struct hold expectation field, err, and result UpdateItemExpectation struct { - attributeUpdates map[string]*dynamodb.AttributeValueUpdate - key map[string]*dynamodb.AttributeValue - table *string - output *dynamodb.UpdateItemOutput + attributeUpdates map[string]*dynamodb.AttributeValueUpdate + key map[string]*dynamodb.AttributeValue + table *string + output *dynamodb.UpdateItemOutput + conditionExpression *string + expressionAttributeNames map[string]*string + expressionAttributeValues map[string]*dynamodb.AttributeValue + updateExpression *string + equivalentUpdateExpression *parsedUpdateExpression } // PutItemExpectation struct hold expectation field, err, and result diff --git a/update_item.go b/update_item.go index 3876a29..285a229 100644 --- a/update_item.go +++ b/update_item.go @@ -2,11 +2,13 @@ package dynamock import ( "fmt" - "reflect" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/dynamodb" + "reflect" + "regexp" + "sort" + "strings" ) // ToTable - method for set Table expectation @@ -21,6 +23,36 @@ func (e *UpdateItemExpectation) WithKeys(keys map[string]*dynamodb.AttributeValu return e } +// WithConditionExpression - method for setting a ConditionExpression expectation +func (e *UpdateItemExpectation) WithConditionExpression(expr *string) *UpdateItemExpectation { + e.conditionExpression = expr + return e +} + +// WithExpressionAttributeNames - method for setting a ExpressionAttributeNames expectation +func (e *UpdateItemExpectation) WithExpressionAttributeNames(names map[string]*string) *UpdateItemExpectation { + e.expressionAttributeNames = names + return e +} + +// WithExpressionAttributeValues - method for setting a ExpressionAttributeValues expectation +func (e *UpdateItemExpectation) WithExpressionAttributeValues(attrs map[string]*dynamodb.AttributeValue) *UpdateItemExpectation { + e.expressionAttributeValues = attrs + return e +} + +// WithUpdateExpression - method for setting a UpdateExpression expectation +func (e *UpdateItemExpectation) WithUpdateExpression(expr *string) *UpdateItemExpectation { + e.updateExpression = expr + return e +} + +func (e *UpdateItemExpectation) WithEquivalentUpdateExpression(expr *string) *UpdateItemExpectation { + parsed := parseUpdateExpression(*expr) + e.equivalentUpdateExpression = &parsed + return e +} + // Updates - method for set Updates expectation func (e *UpdateItemExpectation) Updates(attrs map[string]*dynamodb.AttributeValueUpdate) *UpdateItemExpectation { e.attributeUpdates = attrs @@ -40,19 +72,51 @@ func (e *MockDynamoDB) UpdateItem(input *dynamodb.UpdateItemInput) (*dynamodb.Up if x.table != nil { if *x.table != *input.TableName { - return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect table %s but found table %s", *x.table, *input.TableName) + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("expect table %s but found table %s", *x.table, *input.TableName) } } if x.key != nil { if !reflect.DeepEqual(x.key, input.Key) { - return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect key %+v but found key %+v", x.key, input.Key) + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("expect key %+v but found key %+v", x.key, input.Key) } } if x.attributeUpdates != nil { if !reflect.DeepEqual(x.attributeUpdates, input.AttributeUpdates) { - return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect key %+v but found key %+v", x.attributeUpdates, input.AttributeUpdates) + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("expect AttributeUpdates: %+v but got: %+v", x.attributeUpdates, input.AttributeUpdates) + } + } + + if x.conditionExpression != nil { + if !reflect.DeepEqual(x.conditionExpression, input.ConditionExpression) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("expect ConditionExpressions: %+v but got: %+v", x.conditionExpression, input.ConditionExpression) + } + } + + if x.expressionAttributeNames != nil { + if !reflect.DeepEqual(x.expressionAttributeNames, input.ExpressionAttributeNames) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("expect ExpressionAttributeNames: %+v but got: %+v", x.expressionAttributeNames, input.ExpressionAttributeNames) + } + } + + if x.expressionAttributeValues != nil { + if !reflect.DeepEqual(x.expressionAttributeValues, input.ExpressionAttributeValues) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("expect ExpressionAttributeValues: %+v but got: %+v", x.expressionAttributeValues, input.ExpressionAttributeValues) + } + } + + if x.updateExpression != nil { + if !reflect.DeepEqual(x.updateExpression, input.UpdateExpression) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("expect UpdateExpression: %+v but got: %+v", x.updateExpression, input.UpdateExpression) + } + } + + if x.equivalentUpdateExpression != nil { + inputExpr := parseUpdateExpression(*input.UpdateExpression) + err := x.equivalentUpdateExpression.CheckIsEquivalentTo(&inputExpr) + if err != nil { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("non-equivalent update expressions found: %v", err) } } @@ -65,6 +129,192 @@ func (e *MockDynamoDB) UpdateItem(input *dynamodb.UpdateItemInput) (*dynamodb.Up return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Update Item Expectation Not Found") } +type parsedUpdateExpression struct { + ADDExpressions []pathValueExpression + DELETEExpressions []pathValueExpression + REMOVEExpressions []pathExpression + SETExpressions []pathValueExpression +} + +func (p *parsedUpdateExpression) CheckIsEquivalentTo(other *parsedUpdateExpression) error { + sort.Slice(p.ADDExpressions, func(i, j int) bool { + return p.ADDExpressions[i].path < p.ADDExpressions[j].path + }) + sort.Slice(other.ADDExpressions, func(i, j int) bool { + return other.ADDExpressions[i].path < other.ADDExpressions[j].path + }) + if !reflect.DeepEqual(p.ADDExpressions, other.ADDExpressions) { + return fmt.Errorf("ADDExpressions do not match, %v != %v", p.ADDExpressions, other.ADDExpressions) + } + sort.Slice(p.DELETEExpressions, func(i, j int) bool { + return p.DELETEExpressions[i].path < p.DELETEExpressions[j].path + }) + sort.Slice(other.DELETEExpressions, func(i, j int) bool { + return other.DELETEExpressions[i].path < other.DELETEExpressions[j].path + }) + if !reflect.DeepEqual(p.DELETEExpressions, other.DELETEExpressions) { + return fmt.Errorf("DELETEExpressions do not match, %v != %v", p.DELETEExpressions, other.DELETEExpressions) + } + sort.Slice(p.REMOVEExpressions, func(i, j int) bool { + return p.REMOVEExpressions[i].path < p.REMOVEExpressions[j].path + }) + sort.Slice(other.REMOVEExpressions, func(i, j int) bool { + return other.REMOVEExpressions[i].path < other.REMOVEExpressions[j].path + }) + if !reflect.DeepEqual(p.REMOVEExpressions, other.REMOVEExpressions) { + return fmt.Errorf("REMOVEExpressions do not match, %v != %v", p.REMOVEExpressions, other.REMOVEExpressions) + } + sort.Slice(p.SETExpressions, func(i, j int) bool { + return p.SETExpressions[i].path < p.SETExpressions[j].path + }) + sort.Slice(other.SETExpressions, func(i, j int) bool { + return other.SETExpressions[i].path < other.SETExpressions[j].path + }) + if !reflect.DeepEqual(p.SETExpressions, other.SETExpressions) { + return fmt.Errorf("SETExpressions do not match, %v != %v", p.SETExpressions, other.SETExpressions) + } + return nil +} + +type operation string + +const ( + ADD operation = "ADD" + DELETE operation = "DELETE" + REMOVE operation = "REMOVE" + SET operation = "SET" +) + +type operationIndexTuple struct { + Index int + Operation operation +} + +type pathValueExpression struct { + path string + value string +} + +type pathExpression struct { + path string +} + +func mustExtractPathValueExpressions(operation operation, expr string) []pathValueExpression { + var re *regexp.Regexp + var subMatchRe *regexp.Regexp + var result []pathValueExpression + switch operation { + case ADD: + re = regexp.MustCompile(`ADD\s+((\S+\s+[\w:#]+\s*,?\s*)+)`) + subMatchRe = regexp.MustCompile(`(\S+)\s+([\w:#]+)\s*,?\s*`) + case DELETE: + re = regexp.MustCompile(`DELETE\s+((\S+\s+[\w:#]+\s*,?\s*)+)`) + subMatchRe = regexp.MustCompile(`(\S+)\s+([\w:#]+)\s*,?\s*`) + case SET: + // SET operations allow the value to two operands with a + or - in between them + // SET operations allow the operand to be a function such as `SET #ri = list_append(#ri, :vals)` + re = regexp.MustCompile(`SET\s+((\S+\s*=\s*[\w:#\(\)\+-,\s=]+\s*,?\s*)+)`) + subMatchRe = regexp.MustCompile(`(\S+)\s*=\s*([\w:#\+-]+(\([\w\s,:#]*\))?)\s*,?\s*`) + } + if re == nil { + return result + } + if subMatchRe == nil { + return result + } + + subMatches := re.FindStringSubmatch(expr) + + if subMatches == nil { + return result + } + + pairMatches := subMatchRe.FindAllStringSubmatch(subMatches[1], -1) + if pairMatches == nil { + return result + } + for _, subMatch := range pairMatches { + result = append(result, pathValueExpression{subMatch[1], subMatch[2]}) + } + return result +} + +func extractAddPathValuePairs(addExpr string) []pathValueExpression { + return mustExtractPathValueExpressions(ADD, addExpr) +} + +func extractDeletePathValuePairs(deleteExpr string) []pathValueExpression { + return mustExtractPathValueExpressions(DELETE, deleteExpr) +} + +func extractRemovePath(removeExpr string) []pathExpression { + re := regexp.MustCompile(`REMOVE\s+(([\w:#\[\]]+\s*,?\s*)+)`) + subMatchRe := regexp.MustCompile(`\s*([\w:#\[\]]+)\s*,?\s*`) + subMatches := re.FindStringSubmatch(removeExpr) + var result []pathExpression + if subMatches == nil { + return result + } + pairMatches := subMatchRe.FindAllStringSubmatch(subMatches[1], -1) + if pairMatches == nil { + return result + } + for _, subMatch := range pairMatches { + result = append(result, pathExpression{subMatch[1]}) + } + return result +} + +func extractSetPathValuePairs(setExpr string) []pathValueExpression { + return mustExtractPathValueExpressions(SET, setExpr) +} + +func parseUpdateExpression(updateExpression string) parsedUpdateExpression { + addOp := operationIndexTuple{strings.Index(updateExpression, "ADD"), ADD} + deleteOp := operationIndexTuple{strings.Index(updateExpression, "DELETE"), DELETE} + removeOp := operationIndexTuple{strings.Index(updateExpression, "REMOVE"), REMOVE} + setOp := operationIndexTuple{strings.Index(updateExpression, "SET"), SET} + + ops := []operationIndexTuple{ + addOp, + deleteOp, + removeOp, + setOp, + } + sort.Slice(ops, func(i, j int) bool { + return ops[i].Index < ops[j].Index + }) + + result := parsedUpdateExpression{} + for opIdx, op := range ops { + if op.Index < 0 { + // op.Index should be -1 for operations that are not present in an update expression + continue + } + // get the substring for the operation + var substr string + if opIdx+1 < len(ops) { + // We don't need to worry about the case where (opIdx+1).Index is -1, because we're iterating through a + // ascending sorted array. + substr = updateExpression[op.Index:ops[opIdx+1].Index] + } else { + substr = updateExpression[op.Index:] + } + // apply the operation specific parsing + switch op.Operation { + case ADD: + result.ADDExpressions = extractAddPathValuePairs(substr) + case DELETE: + result.DELETEExpressions = extractDeletePathValuePairs(substr) + case REMOVE: + result.REMOVEExpressions = extractRemovePath(substr) + case SET: + result.SETExpressions = extractSetPathValuePairs(substr) + } + } + return result +} + // UpdateItemWithContext - this func will be invoked when test running matching expectation with actual input func (e *MockDynamoDB) UpdateItemWithContext(ctx aws.Context, input *dynamodb.UpdateItemInput, opt ...request.Option) (*dynamodb.UpdateItemOutput, error) { if len(e.dynaMock.UpdateItemExpect) > 0 { @@ -88,6 +338,36 @@ func (e *MockDynamoDB) UpdateItemWithContext(ctx aws.Context, input *dynamodb.Up } } + if x.conditionExpression != nil { + if !reflect.DeepEqual(x.conditionExpression, input.ConditionExpression) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect key %+v but found key %+v", x.conditionExpression, input.ConditionExpression) + } + } + + if x.expressionAttributeNames != nil { + if !reflect.DeepEqual(x.expressionAttributeNames, input.ExpressionAttributeNames) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect key %+v but found key %+v", x.expressionAttributeNames, input.ExpressionAttributeNames) + } + } + + if x.expressionAttributeValues != nil { + if !reflect.DeepEqual(x.expressionAttributeValues, input.ExpressionAttributeValues) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect key %+v but found key %+v", x.expressionAttributeValues, input.ExpressionAttributeValues) + } + } + + if x.updateExpression != nil { + if !reflect.DeepEqual(x.updateExpression, input.UpdateExpression) { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect key %+v but found key %+v", x.updateExpression, input.UpdateExpression) + } + } + if x.equivalentUpdateExpression != nil { + inputExpr := parseUpdateExpression(*input.UpdateExpression) + err := x.equivalentUpdateExpression.CheckIsEquivalentTo(&inputExpr) + if err != nil { + return &dynamodb.UpdateItemOutput{}, fmt.Errorf("non-equivalent update expressions found: %v", err) + } + } // delete first element of expectation e.dynaMock.UpdateItemExpect = append(e.dynaMock.UpdateItemExpect[:0], e.dynaMock.UpdateItemExpect[1:]...) diff --git a/update_item_test.go b/update_item_test.go new file mode 100644 index 0000000..1eca176 --- /dev/null +++ b/update_item_test.go @@ -0,0 +1,269 @@ +package dynamock + +import ( + "reflect" + "testing" +) + +func Test_mustExtractPathValueExpressions(t *testing.T) { + type args struct { + operation operation + expr string + } + tests := []struct { + name string + args args + want []pathValueExpression + }{ + { + "no matching expression produces empty result", + args{ADD, "SET foo = 3"}, + []pathValueExpression{}, + }, + { + "ADD with single pair captures correct pair", + args{ADD, "ADD foobar 3"}, + []pathValueExpression{{"foobar", "3"}}, + }, + { + "ADD with single pair and trailing comma captures correct pair", + args{ADD, "ADD foobar 3 ,"}, + []pathValueExpression{{"foobar", "3"}}, + }, + { + "ADD with single pair and trailing whitespace captures correct pair", + args{ADD, "ADD foobar 3 "}, + []pathValueExpression{{"foobar", "3"}}, + }, + { + "ADD with multiple pairs captures the pairs", + args{ADD, "ADD foobar 3, bazdog 7, chicken 8"}, + []pathValueExpression{ + {"foobar", "3"}, + {"bazdog", "7"}, + {"chicken", "8"}, + }, + }, + { + "ADD with multiple pairs and curious whitespace", + args{ADD, "ADD foobar 3 , bazdog 7 ,chicken 8"}, + []pathValueExpression{ + {"foobar", "3"}, + {"bazdog", "7"}, + {"chicken", "8"}, + }, + }, + { + "DELETE with single pair and trailing comma captures correct pair", + args{DELETE, "DELETE foobar 3 ,"}, + []pathValueExpression{{"foobar", "3"}}, + }, + { + "SET with single pair captures correct pair", + args{SET, "SET foobar = 3"}, + []pathValueExpression{{"foobar", "3"}}, + }, + { + "SET with multiple pairs captures the pairs", + args{SET, "SET foobar= 3, bazdog =7, chicken=8 "}, + []pathValueExpression{ + {"foobar", "3"}, + {"bazdog", "7"}, + {"chicken", "8"}, + }, + }, + { + "SET with single pair including function captures correct pair", + args{SET, "SET foobar = list_append(:vals, #ri)"}, + []pathValueExpression{{"foobar", "list_append(:vals, #ri)"}}, + }, + { + "SET with multiple pairs including function captures correct pair", + args{SET, "SET dog = :food, foobar = list_append(:vals, #ri)"}, + []pathValueExpression{ + {"dog", ":food"}, + {"foobar", "list_append(:vals, #ri)"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := mustExtractPathValueExpressions(tt.args.operation, tt.args.expr) + if len(got) != len(tt.want) { + t.Errorf("mustExtractPathValueExpressions() len = %v, wanted len %v", len(got), len(tt.want)) + return + } + for idx, pair := range got { + if !reflect.DeepEqual(pair, tt.want[idx]) { + t.Errorf("mustExtractPathValueExpressions() %vth item got: %v, want: %v", idx, pair, tt.want[idx]) + return + } + } + }) + } +} + +func Test_parseUpdateExpression(t *testing.T) { + type args struct { + updateExpression string + } + tests := []struct { + name string + args args + want parsedUpdateExpression + }{ + { + "ADD expression only has only Add expressions", + args{"ADD foobar 3, dog 76"}, + parsedUpdateExpression{ + ADDExpressions: []pathValueExpression{ + {"foobar", "3"}, + {"dog", "76"}, + }, + DELETEExpressions: nil, + REMOVEExpressions: nil, + SETExpressions: nil, + }, + }, + { + "DELETE expression only has only Delete expressions", + args{"DELETE foobar 5, cat 6"}, + parsedUpdateExpression{ + ADDExpressions: nil, + DELETEExpressions: []pathValueExpression{ + {"foobar", "5"}, + {"cat", "6"}, + }, + REMOVEExpressions: nil, + SETExpressions: nil, + }, + }, + { + "ADD and DELETE expressions are extracted correctly", + args{"ADD abc 1, def 2 DELETE ghi 3, jkl 4"}, + parsedUpdateExpression{ + ADDExpressions: []pathValueExpression{ + {"abc", "1"}, + {"def", "2"}, + }, + DELETEExpressions: []pathValueExpression{ + {"ghi", "3"}, + {"jkl", "4"}, + }, + REMOVEExpressions: nil, + SETExpressions: nil, + }, + }, + { + "SET expression only has only Set expressions", + args{"SET foobar = 5, cat = list_append(:vals, #ri)"}, + parsedUpdateExpression{ + ADDExpressions: nil, + DELETEExpressions: nil, + REMOVEExpressions: nil, + SETExpressions: []pathValueExpression{ + {"foobar", "5"}, + {"cat", "list_append(:vals, #ri)"}, + }, + }, + }, + { + "REMOVE expression only has only Remove expressions", + args{"REMOVE RelatedItems[1], RelatedItems[2]"}, + parsedUpdateExpression{ + ADDExpressions: nil, + DELETEExpressions: nil, + REMOVEExpressions: []pathExpression{ + {"RelatedItems[1]"}, + {"RelatedItems[2]"}, + }, + SETExpressions: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseUpdateExpression(tt.args.updateExpression); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseUpdateExpression() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_extractRemovePath(t *testing.T) { + type args struct { + removeExpr string + } + tests := []struct { + name string + args args + want []pathExpression + }{ + { + "extracting a single Remove path works correctly", + args{"REMOVE RelatedItems[1]"}, + []pathExpression{{"RelatedItems[1]"}}, + }, + { + "extracting multiple Remove paths works correctly", + args{"REMOVE RelatedItems[1], RelatedItems[2]"}, + []pathExpression{ + {"RelatedItems[1]"}, + {"RelatedItems[2]"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractRemovePath(tt.args.removeExpr); !reflect.DeepEqual(got, tt.want) { + t.Errorf("extractRemovePath() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parsedUpdateExpression_CheckIsEquivalentTo(t *testing.T) { + type args struct { + p string + other string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + "equivalent expressions do not return an error", + args{ + "ADD foobar 5, dog 7 ", + "ADD dog 7, foobar 5", + }, + false, + }, + { + "non-equivalent expressions return an error", + args{ + "ADD foobar 5, dog 7 ", + "ADD cat 7, foobar 5", + }, + true, + }, + { + "non-equivalent expressions with equivalent paths return an error", + args{ + "ADD foobar 5, dog 7 ", + "ADD dog 7, foobar 99", + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pExpr := parseUpdateExpression(tt.args.p) + otherExpr := parseUpdateExpression(tt.args.other) + if err := pExpr.CheckIsEquivalentTo(&otherExpr); (err != nil) != tt.wantErr { + t.Errorf("CheckIsEquivalentTo() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}