From 6efd3ae921d8a25e8d0b1b3886749d14016684ac Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Thu, 30 Mar 2017 07:04:54 -0400 Subject: [PATCH] Add test for banking responses This also adds a generic response equality testing framework, a missing Status field to all current responses, and Equal() methods to all basic types. --- banking.go | 1 + banking_test.go | 141 +++++++++++++++++++++++++++++++++++++++++++++++ creditcards.go | 1 + profile.go | 1 + request_test.go | 2 +- response_test.go | 131 +++++++++++++++++++++++++++++++++++++++++++ signup.go | 1 + types.go | 25 +++++++++ 8 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 response_test.go diff --git a/banking.go b/banking.go index e785454..99909ad 100644 --- a/banking.go +++ b/banking.go @@ -126,6 +126,7 @@ type Balance struct { type StatementResponse struct { XMLName xml.Name `xml:"STMTTRNRS"` TrnUID UID `xml:"TRNUID"` + Status Status `xml:"STATUS"` CurDef String `xml:"STMTRS>CURDEF"` BankAcctFrom BankAcct `xml:"STMTRS>BANKACCTFROM"` BankTranList *TransactionList `xml:"STMTRS>BANKTRANLIST,omitempty"` diff --git a/banking_test.go b/banking_test.go index e6debf0..c21aa84 100644 --- a/banking_test.go +++ b/banking_test.go @@ -2,6 +2,8 @@ package ofxgo_test import ( "github.com/aclindsa/ofxgo" + "math/big" + "strings" "testing" "time" ) @@ -71,3 +73,142 @@ func TestMarshalBankStatementRequest(t *testing.T) { marshalCheckRequest(t, &request, expectedString) } + +func TestUnmarshalBankStatementResponse(t *testing.T) { + responseReader := strings.NewReader(` + + + + + + 0 + INFO + + 20060115112303 + ENG + 20050221091300 + 20060102160000 + + BNK + 1987 + + + + + + 1001 + + 0 + INFO + + + USD + + 318398732 + 78346129 + CHECKING + + + 20060101 + 20060115 + + CHECK + 20060104 + -200.00 + 00592 + 2002 + + + ATM + 20060112 + 20060112 + -300.00 + 00679 + + + + 200.29 + 200601141600 + + + 200.29 + 200601141600 + + + + +`) + var expected ofxgo.Response + GMT := time.FixedZone("GMT", 0) + + expected.Version = "203" + expected.Signon.Status.Code = 0 + expected.Signon.Status.Severity = "INFO" + expected.Signon.DtServer = ofxgo.Date(time.Date(2006, 1, 15, 11, 23, 03, 0, GMT)) + expected.Signon.Language = "ENG" + dtprofup := ofxgo.Date(time.Date(2005, 2, 21, 9, 13, 0, 0, GMT)) + expected.Signon.DtProfUp = &dtprofup + dtacctup := ofxgo.Date(time.Date(2006, 1, 2, 16, 0, 0, 0, GMT)) + expected.Signon.DtAcctUp = &dtacctup + expected.Signon.Org = "BNK" + expected.Signon.Fid = "1987" + + var trnamt1, trnamt2 big.Rat + trnamt1.SetFrac64(-20000, 100) + trnamt2.SetFrac64(-30000, 100) + dtuser2 := ofxgo.Date(time.Date(2006, 1, 12, 0, 0, 0, 0, GMT)) + + banktranlist := ofxgo.TransactionList{ + DtStart: ofxgo.Date(time.Date(2006, 1, 1, 0, 0, 0, 0, GMT)), + DtEnd: ofxgo.Date(time.Date(2006, 1, 15, 0, 0, 0, 0, GMT)), + Transactions: []ofxgo.Transaction{ + { + TrnType: "CHECK", + DtPosted: ofxgo.Date(time.Date(2006, 1, 4, 0, 0, 0, 0, GMT)), + TrnAmt: ofxgo.Amount(trnamt1), + FiTId: "00592", + CheckNum: "2002", + }, + { + TrnType: "ATM", + DtPosted: ofxgo.Date(time.Date(2006, 1, 12, 0, 0, 0, 0, GMT)), + DtUser: &dtuser2, + TrnAmt: ofxgo.Amount(trnamt2), + FiTId: "00679", + }, + }, + } + + var balamt, availbalamt big.Rat + balamt.SetFrac64(20029, 100) + availbalamt.SetFrac64(20029, 100) + + availdtasof := ofxgo.Date(time.Date(2006, 1, 14, 16, 0, 0, 0, GMT)) + + statementResponse := ofxgo.StatementResponse{ + TrnUID: "1001", + Status: ofxgo.Status{ + Code: 0, + Severity: "INFO", + }, + CurDef: "USD", + BankAcctFrom: ofxgo.BankAcct{ + BankId: "318398732", + AcctId: "78346129", + AcctType: "CHECKING", + }, + BankTranList: &banktranlist, + BalAmt: ofxgo.Amount(balamt), + DtAsOf: ofxgo.Date(time.Date(2006, 1, 14, 16, 0, 0, 0, GMT)), + AvailBalAmt: (*ofxgo.Amount)(&availbalamt), + AvailDtAsOf: &availdtasof, + } + expected.Banking = append(expected.Banking, statementResponse) + + response, err := ofxgo.ParseResponse(responseReader) + if err != nil { + t.Fatalf("Unexpected error unmarshalling response: %s\n", err) + } + + checkResponsesEqual(t, &expected, response) +} diff --git a/creditcards.go b/creditcards.go index 4ef61e7..913c3e6 100644 --- a/creditcards.go +++ b/creditcards.go @@ -28,6 +28,7 @@ func (r *CCStatementRequest) Valid() (bool, error) { type CCStatementResponse struct { XMLName xml.Name `xml:"CCSTMTTRNRS"` TrnUID UID `xml:"TRNUID"` + Status Status `xml:"STATUS"` CurDef String `xml:"CCSTMTRS>CURDEF"` CCAcctFrom CCAcct `xml:"CCSTMTRS>CCACCTFROM"` BankTranList *TransactionList `xml:"CCSTMTRS>BANKTRANLIST,omitempty"` diff --git a/profile.go b/profile.go index e31b43f..4e0a5e8 100644 --- a/profile.go +++ b/profile.go @@ -97,6 +97,7 @@ func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) type ProfileResponse struct { XMLName xml.Name `xml:"PROFTRNRS"` TrnUID UID `xml:"TRNUID"` + Status Status `xml:"STATUS"` MessageSetList MessageSetList `xml:"PROFRS>MSGSETLIST"` SignonInfoList []SignonInfo `xml:"PROFRS>SIGNONINFOLIST>SIGNONINFO"` DtProfUp Date `xml:"PROFRS>DTPROFUP"` diff --git a/request_test.go b/request_test.go index e989257..b1d9531 100644 --- a/request_test.go +++ b/request_test.go @@ -11,7 +11,7 @@ var ignoreSpacesRe = regexp.MustCompile(">[ \t\r\n]+<") func marshalCheckRequest(t *testing.T, request *ofxgo.Request, expected string) { buf, err := request.Marshal() if err != nil { - t.Fatalf("Unexpected error marshalling request: %s\n", err) + t.Fatalf("%s: Unexpected error marshalling request: %s\n", t.Name(), err) } actualString := buf.String() diff --git a/response_test.go b/response_test.go new file mode 100644 index 0000000..d7a5b64 --- /dev/null +++ b/response_test.go @@ -0,0 +1,131 @@ +package ofxgo_test + +import ( + "fmt" + "github.com/aclindsa/go/src/encoding/xml" + "github.com/aclindsa/ofxgo" + "reflect" + "testing" +) + +// Attempt to find a method on the provided Value called 'Equal' which is a +// receiver for the Value, takes one argument of the same type, and returns +// one bool. equalMethodOf() returns the nil value if the method couldn't be +// found. +func equalMethodOf(v reflect.Value) reflect.Value { + if equalMethod, ok := v.Type().MethodByName("Equal"); ok { + if !equalMethod.Func.IsNil() && + equalMethod.Type.NumIn() == 2 && + equalMethod.Type.In(0) == v.Type() && + equalMethod.Type.In(1) == v.Type() && + equalMethod.Type.NumOut() == 1 && + equalMethod.Type.Out(0).Kind() == reflect.Bool { + return v.MethodByName("Equal") + } + } + return reflect.ValueOf(nil) +} + +// Attempt to return a string representation of the value appropriate for its +// type by finding a method on the provided Value called 'String' which is a +// receiver for the Value, and returns one string. stringMethodOf() returns +// fmt.Sprintf("%s", v) if it can't find a String method. +func valueToString(v reflect.Value) string { + if equalMethod, ok := v.Type().MethodByName("String"); ok { + if !equalMethod.Func.IsNil() && + equalMethod.Type.NumIn() == 1 && + equalMethod.Type.In(0) == v.Type() && + equalMethod.Type.NumOut() == 1 && + equalMethod.Type.Out(0).Kind() == reflect.String { + out := v.MethodByName("String").Call([]reflect.Value{}) + return out[0].String() + } + } + return fmt.Sprintf("%s", v) +} + +// Recursively check that the expected and actual Values are equal in value. +// If the two Values are equal in type and contain an appropriate Equal() +// method (see equalMethodOf()), that method is used for comparison. The +// provided testing.T is failed with a message if any inequality is found. +func checkEqual(t *testing.T, fieldName string, expected, actual reflect.Value) { + if expected.IsValid() && !actual.IsValid() { + t.Fatalf("%s: %s was unexpectedly nil\n", t.Name(), fieldName) + } else if !expected.IsValid() && actual.IsValid() { + t.Fatalf("%s: Expected %s to be nil (it wasn't)\n", t.Name(), fieldName) + } else if !expected.IsValid() && !actual.IsValid() { + return + } + + if expected.Type() != actual.Type() { + t.Fatalf("%s: Expected %s type for %s, found %s\n", t.Name(), expected.Type().Name(), fieldName, actual.Type().Name()) + } + + equalMethod := equalMethodOf(expected) + if equalMethod.IsValid() { + in := []reflect.Value{actual} + out := equalMethod.Call(in) + if !out[0].Bool() { + t.Fatalf("%s: %s !Equal(): expected '%s', got '%s'\n", t.Name(), fieldName, valueToString(expected), valueToString(actual)) + } + return + } + + switch expected.Kind() { + case reflect.Array: + for i := 0; i < expected.Len(); i++ { + checkEqual(t, fmt.Sprintf("%s[%d]", fieldName, i), expected.Index(i), actual.Index(i)) + } + case reflect.Slice: + if !expected.IsNil() && actual.IsNil() { + t.Fatalf("%s: %s was unexpectedly nil\n", t.Name(), fieldName) + } else if expected.IsNil() && !actual.IsNil() { + t.Fatalf("%s: Expected %s to be nil (it wasn't)\n", t.Name(), fieldName) + } + if expected.Len() != actual.Len() { + t.Fatalf("%s: Expected len(%s) to to be %d, was %d\n", t.Name(), fieldName, expected.Len(), actual.Len()) + } + for i := 0; i < expected.Len(); i++ { + checkEqual(t, fmt.Sprintf("%s[%d]", fieldName, i), expected.Index(i), actual.Index(i)) + } + case reflect.Interface: + if !expected.IsNil() && actual.IsNil() { + t.Fatalf("%s: %s was unexpectedly nil\n", t.Name(), fieldName) + } else if expected.IsNil() && !actual.IsNil() { + t.Fatalf("%s: Expected %s to be nil (it wasn't)\n", t.Name(), fieldName) + } + checkEqual(t, fieldName, expected.Elem(), actual.Elem()) + case reflect.Ptr: + checkEqual(t, fieldName, expected.Elem(), actual.Elem()) + case reflect.Struct: + structType := expected.Type() + for i, n := 0, expected.NumField(); i < n; i++ { + field := structType.Field(i) + // skip XMLName fields so we can be lazy and not fill them out in + // testing code + var xmlname xml.Name + if field.Name == "XMLName" && field.Type == reflect.TypeOf(xmlname) { + continue + } + + // Construct a new field name for this field, containing the parent + // fieldName + newFieldName := fieldName + if fieldName != "" { + newFieldName = fieldName + "." + } + newFieldName = newFieldName + field.Name + checkEqual(t, newFieldName, expected.Field(i), actual.Field(i)) + } + case reflect.String: + if expected.String() != actual.String() { + t.Fatalf("%s: %s expected to be '%s', found '%s'\n", t.Name(), fieldName, expected.String(), actual.String()) + } + default: + t.Fatalf("%s: %s has unexpected type that didn't provide an Equal() method: %s\n", t.Name(), fieldName, expected.Type().Name()) + } +} + +func checkResponsesEqual(t *testing.T, expected, actual *ofxgo.Response) { + checkEqual(t, "", reflect.ValueOf(expected), reflect.ValueOf(actual)) +} diff --git a/signup.go b/signup.go index 7c594b0..ae150dc 100644 --- a/signup.go +++ b/signup.go @@ -107,6 +107,7 @@ type AcctInfo struct { type AcctInfoResponse struct { XMLName xml.Name `xml:"ACCTINFOTRNRS"` TrnUID UID `xml:"TRNUID"` + Status Status `xml:"STATUS"` DtAcctUp Date `xml:"ACCTINFORS>DTACCTUP"` AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"` } diff --git a/types.go b/types.go index 0414cf7..7f4fe2d 100644 --- a/types.go +++ b/types.go @@ -33,6 +33,10 @@ func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } +func (i Int) Equal(o Int) bool { + return i == o +} + type Amount big.Rat func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { @@ -66,6 +70,11 @@ func (a *Amount) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(a.String(), start) } +func (a Amount) Equal(o Amount) bool { + ratA := (*big.Rat)(&a) + return ratA.Cmp((*big.Rat)(&o)) == 0 +} + type Date time.Time var ofxDateFormats = []string{ @@ -167,6 +176,10 @@ func (od *Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(od.String(), start) } +func (od Date) Equal(o Date) bool { + return time.Time(od).Equal(time.Time(o)) +} + type String string func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { @@ -183,6 +196,10 @@ func (os *String) String() string { return string(*os) } +func (os String) Equal(o String) bool { + return os == o +} + type Boolean bool func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { @@ -214,6 +231,10 @@ func (ob *Boolean) String() string { return fmt.Sprintf("%v", *ob) } +func (ob Boolean) Equal(o Boolean) bool { + return ob == o +} + type UID string // The OFX specification recommends that UIDs follow the standard UUID @@ -228,6 +249,10 @@ func (ou UID) RecommendedFormat() (bool, error) { return true, nil } +func (ou UID) Equal(o UID) bool { + return ou == o +} + func RandomUID() (*UID, error) { uidbytes := make([]byte, 16) n, err := rand.Read(uidbytes[:])