Repository: coinbase/rosetta-sdk-go
Pull request #83 (2020-07-25 17:23:18)

Assert Successful Intent
### Changes
* Modify `ExpectedOperations` to allow for asserting success on matched operations
* Print out all missing matches in `ExpectedOperations` error


Patch:
diff --git a/parser/intent.go b/parser/intent.go
index 51d5e34..88df89a 100644
--- a/parser/intent.go
+++ b/parser/intent.go
@@ -15,6 +15,7 @@
 package parser
 
 import (
+	"errors"
 	"fmt"
 	"log"
 
@@ -57,13 +58,16 @@ func ExpectedOperation(intent *types.Operation, observed *types.Operation) error
 // ExpectedOperations returns an error if a slice of intended
 // operations differ from observed operations. Optionally,
 // it is possible to error if any extra observed opertions
-// are found.
-func ExpectedOperations(
+// are found or if operations matched are not considered
+// successful.
+func (p *Parser) ExpectedOperations(
 	intent []*types.Operation,
 	observed []*types.Operation,
 	errExtra bool,
+	confirmSuccess bool,
 ) error {
 	matches := make(map[int]struct{})
+	failedMatches := []*types.Operation{}
 	unmatched := []*types.Operation{}
 
 	for _, obs := range observed {
@@ -73,11 +77,28 @@ func ExpectedOperations(
 				continue
 			}
 
-			if ExpectedOperation(in, obs) == nil {
-				matches[i] = struct{}{}
-				foundMatch = true
-				break
+			// Any error returned here only indicated that intent
+			// does not match observed. For ExpectedOperations,
+			// we don't care about the content of the error, we
+			// just care if it errors so we can evaluate the next
+			// operation for a match.
+			if err := ExpectedOperation(in, obs); err != nil {
+				continue
+			}
+
+			obsSuccess, err := p.Asserter.OperationSuccessful(obs)
+			if err != nil {
+				return fmt.Errorf("%w: unable to check operation success", err)
 			}
+
+			if confirmSuccess && !obsSuccess {
+				failedMatches = append(failedMatches, obs)
+				continue
+			}
+
+			matches[i] = struct{}{}
+			foundMatch = true
+			break
 		}
 
 		if !foundMatch {
@@ -91,13 +112,28 @@ func ExpectedOperations(
 		}
 	}
 
+	missingIntent := []int{}
 	for i := 0; i < len(intent); i++ {
 		if _, exists := matches[i]; !exists {
-			return fmt.Errorf(
-				"could not find operation with intent %s in observed",
-				types.PrettyPrintStruct(intent[i]),
+			missingIntent = append(missingIntent, i)
+		}
+	}
+
+	if len(missingIntent) > 0 {
+		errString := fmt.Sprintf(
+			"could intent match for %v",
+			missingIntent,
+		)
+
+		if len(failedMatches) > 0 {
+			errString += fmt.Sprintf(
+				"%s: found matching ops with unsuccessful status %s",
+				errString,
+				types.PrettyPrintStruct(failedMatches),
 			)
 		}
+
+		return errors.New(errString)
 	}
 
 	if len(unmatched) > 0 {


Test patch:
diff --git a/parser/intent_test.go b/parser/intent_test.go
index ab678ce..11a3389 100644
--- a/parser/intent_test.go
+++ b/parser/intent_test.go
@@ -175,9 +175,10 @@ func TestExpectedOperation(t *testing.T) {
 
 func TestExpectedOperations(t *testing.T) {
 	var tests = map[string]struct {
-		intent   []*types.Operation
-		observed []*types.Operation
-		errExtra bool
+		intent         []*types.Operation
+		observed       []*types.Operation
+		errExtra       bool
+		confirmSuccess bool
 
 		err bool
 	}{
@@ -242,6 +243,131 @@ func TestExpectedOperations(t *testing.T) {
 				},
 			},
 		},
+		"simple match (confirm success)": {
+			intent: []*types.Operation{
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 1,
+					},
+					Type: "transfer",
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+					},
+					Amount: &types.Amount{
+						Value: "100",
+					},
+				},
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 5,
+					},
+					Type: "fee",
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+					Amount: &types.Amount{
+						Value: "50",
+					},
+				},
+			},
+			observed: []*types.Operation{
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 2,
+					},
+					Status: "success",
+					Type:   "fee",
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+					Amount: &types.Amount{
+						Value: "50",
+					},
+				},
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 3,
+					},
+					RelatedOperations: []*types.OperationIdentifier{
+						{
+							Index: 2,
+						},
+					},
+					Status: "success",
+					Type:   "transfer",
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+					},
+					Amount: &types.Amount{
+						Value: "100",
+					},
+				},
+			},
+			confirmSuccess: true,
+		},
+		"simple match (confirm success) err": {
+			intent: []*types.Operation{
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 1,
+					},
+					Type: "transfer",
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+					},
+					Amount: &types.Amount{
+						Value: "100",
+					},
+				},
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 5,
+					},
+					Type: "fee",
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+					Amount: &types.Amount{
+						Value: "50",
+					},
+				},
+			},
+			observed: []*types.Operation{
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 2,
+					},
+					Status: "success",
+					Type:   "fee",
+					Account: &types.AccountIdentifier{
+						Address: "addr2",
+					},
+					Amount: &types.Amount{
+						Value: "50",
+					},
+				},
+				{
+					OperationIdentifier: &types.OperationIdentifier{
+						Index: 3,
+					},
+					RelatedOperations: []*types.OperationIdentifier{
+						{
+							Index: 2,
+						},
+					},
+					Status: "failure",
+					Type:   "transfer",
+					Account: &types.AccountIdentifier{
+						Address: "addr1",
+					},
+					Amount: &types.Amount{
+						Value: "100",
+					},
+				},
+			},
+			confirmSuccess: true,
+			err:            true,
+		},
 		"err extra": {
 			intent: []*types.Operation{
 				{
@@ -345,8 +471,28 @@ func TestExpectedOperations(t *testing.T) {
 	}
 
 	for name, test := range tests {
+		asserter, err := simpleAsserterConfiguration([]*types.OperationStatus{
+			{
+				Status:     "success",
+				Successful: true,
+			},
+			{
+				Status:     "failure",
+				Successful: false,
+			},
+		})
+		assert.NoError(t, err)
+		assert.NotNil(t, asserter)
+
+		parser := New(asserter, nil)
+
 		t.Run(name, func(t *testing.T) {
-			err := ExpectedOperations(test.intent, test.observed, test.errExtra)
+			err := parser.ExpectedOperations(
+				test.intent,
+				test.observed,
+				test.errExtra,
+				test.confirmSuccess,
+			)
 			if test.err {
 				assert.Error(t, err)
 			} else {