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.
This commit is contained in:
Aaron Lindsay 2017-03-30 07:04:54 -04:00
parent 6d6ee3ea1b
commit 6efd3ae921
8 changed files with 302 additions and 1 deletions

View File

@ -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"`

View File

@ -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(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?OFX OFXHEADER="200" VERSION="203" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>
<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<DTSERVER>20060115112303</DTSERVER>
<LANGUAGE>ENG</LANGUAGE>
<DTPROFUP>20050221091300</DTPROFUP>
<DTACCTUP>20060102160000</DTACCTUP>
<FI>
<ORG>BNK</ORG>
<FID>1987</FID>
</FI>
</SONRS>
</SIGNONMSGSRSV1>
<BANKMSGSRSV1>
<STMTTRNRS>
<TRNUID>1001</TRNUID>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<STMTRS>
<CURDEF>USD</CURDEF>
<BANKACCTFROM>
<BANKID>318398732</BANKID>
<ACCTID>78346129</ACCTID>
<ACCTTYPE>CHECKING</ACCTTYPE>
</BANKACCTFROM>
<BANKTRANLIST>
<DTSTART>20060101</DTSTART>
<DTEND>20060115</DTEND>
<STMTTRN>
<TRNTYPE>CHECK</TRNTYPE>
<DTPOSTED>20060104</DTPOSTED>
<TRNAMT>-200.00</TRNAMT>
<FITID>00592</FITID>
<CHECKNUM>2002</CHECKNUM>
</STMTTRN>
<STMTTRN>
<TRNTYPE>ATM</TRNTYPE>
<DTPOSTED>20060112</DTPOSTED>
<DTUSER>20060112</DTUSER>
<TRNAMT>-300.00</TRNAMT>
<FITID>00679</FITID>
</STMTTRN>
</BANKTRANLIST>
<LEDGERBAL>
<BALAMT>200.29</BALAMT>
<DTASOF>200601141600</DTASOF>
</LEDGERBAL>
<AVAILBAL>
<BALAMT>200.29</BALAMT>
<DTASOF>200601141600</DTASOF>
</AVAILBAL>
</STMTRS>
</STMTTRNRS>
</BANKMSGSRSV1>
</OFX>`)
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)
}

View File

@ -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"`

View File

@ -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"`

View File

@ -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()

131
response_test.go Normal file
View File

@ -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))
}

View File

@ -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"`
}

View File

@ -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[:])