2020-03-31 00:19:18 -04:00
|
|
|
package ofxgo
|
2017-03-30 07:04:54 -04:00
|
|
|
|
|
|
|
import (
|
2020-03-25 01:40:03 -04:00
|
|
|
"bytes"
|
|
|
|
"errors"
|
2017-03-30 07:04:54 -04:00
|
|
|
"fmt"
|
2017-04-10 20:01:01 -04:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2017-03-30 07:04:54 -04:00
|
|
|
"reflect"
|
|
|
|
"testing"
|
2020-03-25 01:40:03 -04:00
|
|
|
|
|
|
|
"github.com/aclindsa/xml"
|
2017-03-30 07:04:54 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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() {
|
2017-03-31 11:54:43 -04:00
|
|
|
t.Fatalf("%s: Expected %s type for %s, found %s\n", t.Name(), expected.Type(), fieldName, actual.Type())
|
2017-03-30 07:04:54 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
|
|
|
}
|
2017-04-06 05:58:22 -04:00
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
if expected.Uint() != actual.Uint() {
|
|
|
|
t.Fatalf("%s: %s expected to be '%s', found '%s'\n", t.Name(), fieldName, valueToString(expected), valueToString(actual))
|
|
|
|
}
|
2017-03-30 07:04:54 -04:00
|
|
|
default:
|
|
|
|
t.Fatalf("%s: %s has unexpected type that didn't provide an Equal() method: %s\n", t.Name(), fieldName, expected.Type().Name())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-31 00:19:18 -04:00
|
|
|
func checkResponsesEqual(t *testing.T, expected, actual *Response) {
|
2017-03-30 07:04:54 -04:00
|
|
|
checkEqual(t, "", reflect.ValueOf(expected), reflect.ValueOf(actual))
|
|
|
|
}
|
2017-04-10 20:01:01 -04:00
|
|
|
|
2020-03-31 00:19:18 -04:00
|
|
|
func checkResponseRoundTrip(t *testing.T, response *Response) {
|
2019-03-01 22:40:49 -05:00
|
|
|
b, err := response.Marshal()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Unexpected error re-marshaling OFX response: %s\n", err)
|
|
|
|
}
|
2020-03-31 00:19:18 -04:00
|
|
|
roundtripped, err := ParseResponse(b)
|
2019-03-01 22:40:49 -05:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Unexpected error re-parsing OFX response: %s\n", err)
|
|
|
|
}
|
|
|
|
checkResponsesEqual(t, response, roundtripped)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that these samples both parse without errors, and can be converted
|
|
|
|
// back and forth without changing.
|
2017-04-10 20:01:01 -04:00
|
|
|
func TestValidSamples(t *testing.T) {
|
|
|
|
fn := func(path string, info os.FileInfo, err error) error {
|
|
|
|
if info.IsDir() {
|
|
|
|
return nil
|
2019-01-04 11:18:55 -05:00
|
|
|
} else if ext := filepath.Ext(path); ext != ".ofx" && ext != ".qfx" {
|
2017-04-10 20:01:01 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Unexpected error opening %s: %s\n", path, err)
|
|
|
|
}
|
2020-03-31 00:19:18 -04:00
|
|
|
response, err := ParseResponse(file)
|
2017-04-10 20:01:01 -04:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Unexpected error parsing OFX response in %s: %s\n", path, err)
|
|
|
|
}
|
2019-03-01 22:40:49 -05:00
|
|
|
checkResponseRoundTrip(t, response)
|
2017-04-10 20:01:01 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
filepath.Walk("samples/valid_responses", fn)
|
2019-01-04 11:18:55 -05:00
|
|
|
filepath.Walk("samples/busted_responses", fn)
|
2017-04-10 20:01:01 -04:00
|
|
|
}
|
2020-03-25 01:40:03 -04:00
|
|
|
|
|
|
|
func TestInvalidResponse(t *testing.T) {
|
|
|
|
// in this example, the severity is invalid due to mixed upper and lower case letters
|
2020-03-31 01:29:11 -04:00
|
|
|
const invalidResponse = `
|
2020-03-25 01:40:03 -04:00
|
|
|
OFXHEADER:100
|
|
|
|
DATA:OFXSGML
|
|
|
|
VERSION:102
|
|
|
|
SECURITY:NONE
|
|
|
|
ENCODING:USASCII
|
|
|
|
CHARSET:1252
|
|
|
|
COMPRESSION:NONE
|
|
|
|
OLDFILEUID:NONE
|
|
|
|
NEWFILEUID:NONE
|
|
|
|
|
|
|
|
<OFX>
|
|
|
|
<SIGNONMSGSRSV1>
|
|
|
|
<SONRS>
|
|
|
|
<STATUS>
|
|
|
|
<CODE>0</CODE>
|
|
|
|
<SEVERITY>Info</SEVERITY>
|
|
|
|
</STATUS>
|
|
|
|
<LANGUAGE>ENG</LANGUAGE>
|
|
|
|
</SONRS>
|
|
|
|
</SIGNONMSGSRSV1>
|
|
|
|
<BANKMSGSRSV1>
|
|
|
|
<STMTTRNRS>
|
|
|
|
<TRNUID>0</TRNUID>
|
|
|
|
<STATUS>
|
|
|
|
<CODE>0</CODE>
|
|
|
|
<SEVERITY>Info</SEVERITY>
|
|
|
|
</STATUS>
|
|
|
|
</STMTTRNRS>
|
|
|
|
</BANKMSGSRSV1>
|
|
|
|
</OFX>
|
2020-03-31 01:29:11 -04:00
|
|
|
`
|
|
|
|
const expectedErr = "Validation failed: Invalid STATUS>SEVERITY; Invalid STATUS>SEVERITY"
|
|
|
|
|
|
|
|
t.Run("parse response", func(t *testing.T) {
|
2020-03-31 00:19:18 -04:00
|
|
|
resp, err := ParseResponse(bytes.NewReader([]byte(invalidResponse)))
|
2020-03-31 01:29:11 -04:00
|
|
|
expectedErr := "Validation failed: Invalid STATUS>SEVERITY; Invalid STATUS>SEVERITY"
|
|
|
|
if err == nil {
|
|
|
|
t.Fatalf("ParseResponse should fail with %q, found nil", expectedErr)
|
|
|
|
}
|
2020-03-31 00:19:18 -04:00
|
|
|
if _, ok := err.(errInvalid); !ok {
|
2020-03-31 01:29:11 -04:00
|
|
|
t.Errorf("ParseResponse should return an error with type ErrInvalid, found %T", err)
|
|
|
|
}
|
|
|
|
if err.Error() != expectedErr {
|
|
|
|
t.Errorf("ParseResponse should fail with %q, found %v", expectedErr, err)
|
|
|
|
}
|
|
|
|
if resp == nil {
|
|
|
|
t.Errorf("Response must not be nil if only validation errors are present")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("parse failed", func(t *testing.T) {
|
2020-03-31 00:19:18 -04:00
|
|
|
resp, err := ParseResponse(bytes.NewReader(nil))
|
2020-03-31 01:29:11 -04:00
|
|
|
if err == nil {
|
|
|
|
t.Error("ParseResponse should fail to decode")
|
|
|
|
}
|
|
|
|
if resp != nil {
|
|
|
|
t.Errorf("ParseResponse should return a nil response, found: %v", resp)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("decode, then validate response", func(t *testing.T) {
|
2020-03-31 00:19:18 -04:00
|
|
|
resp, err := DecodeResponse(bytes.NewReader([]byte(invalidResponse)))
|
2020-03-31 01:29:11 -04:00
|
|
|
if err != nil {
|
|
|
|
t.Errorf("Unexpected error: %s", err.Error())
|
|
|
|
}
|
|
|
|
if resp == nil {
|
|
|
|
t.Fatal("Response should not be nil from successful decode")
|
|
|
|
}
|
|
|
|
valid, err := resp.Valid()
|
|
|
|
if valid {
|
|
|
|
t.Error("Response should not be valid")
|
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
t.Fatalf("response.Valid() should fail with %q, found nil", expectedErr)
|
|
|
|
}
|
2020-03-31 00:19:18 -04:00
|
|
|
if _, ok := err.(errInvalid); !ok {
|
2020-03-31 01:29:11 -04:00
|
|
|
t.Errorf("response.Valid() should return an error of type ErrInvalid, found: %T", err)
|
|
|
|
}
|
|
|
|
if err.Error() != expectedErr {
|
|
|
|
t.Errorf("response.Valid() should return an error with message %q, but found %q", expectedErr, err.Error())
|
|
|
|
}
|
|
|
|
})
|
2020-03-25 01:40:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestErrInvalidError(t *testing.T) {
|
|
|
|
expectedErr := `Validation failed: A; B; C`
|
2020-03-31 00:19:18 -04:00
|
|
|
actualErr := errInvalid{
|
2020-03-25 01:40:03 -04:00
|
|
|
errors.New("A"),
|
|
|
|
errors.New("B"),
|
|
|
|
errors.New("C"),
|
|
|
|
}.Error()
|
|
|
|
if expectedErr != actualErr {
|
|
|
|
t.Errorf("Unexpected invalid error message to be %q, but was: %s", expectedErr, actualErr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestErrInvalidAddErr(t *testing.T) {
|
|
|
|
t.Run("nil error should be a no-op", func(t *testing.T) {
|
2020-03-31 00:19:18 -04:00
|
|
|
var errs errInvalid
|
2020-03-25 01:40:03 -04:00
|
|
|
errs.AddErr(nil)
|
|
|
|
if len(errs) != 0 {
|
|
|
|
t.Errorf("Nil err should not be added")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("adds an error normally", func(t *testing.T) {
|
2020-03-31 00:19:18 -04:00
|
|
|
var errs errInvalid
|
2020-03-25 01:40:03 -04:00
|
|
|
errs.AddErr(errors.New("some error"))
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("adding the same type should flatten the errors", func(t *testing.T) {
|
2020-03-31 00:19:18 -04:00
|
|
|
var errs errInvalid
|
|
|
|
errs.AddErr(errInvalid{
|
2020-03-25 01:40:03 -04:00
|
|
|
errors.New("A"),
|
|
|
|
errors.New("B"),
|
|
|
|
})
|
2020-03-31 00:19:18 -04:00
|
|
|
errs.AddErr(errInvalid{
|
2020-03-25 01:40:03 -04:00
|
|
|
errors.New("C"),
|
|
|
|
})
|
|
|
|
if len(errs) != 3 {
|
|
|
|
t.Errorf("Errors should be flattened like [A, B, C], but found: %+v", errs)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestErrInvalidErrOrNil(t *testing.T) {
|
2020-03-31 00:19:18 -04:00
|
|
|
var errs errInvalid
|
2020-03-25 01:40:03 -04:00
|
|
|
if err := errs.ErrOrNil(); err != nil {
|
|
|
|
t.Errorf("No added errors should return nil, found: %v", err)
|
|
|
|
}
|
|
|
|
someError := errors.New("some error")
|
|
|
|
errs.AddErr(someError)
|
|
|
|
err := errs.ErrOrNil()
|
|
|
|
if err == nil {
|
|
|
|
t.Fatal("Expected an error, found nil.")
|
|
|
|
}
|
2020-03-31 00:19:18 -04:00
|
|
|
if _, ok := err.(errInvalid); !ok {
|
|
|
|
t.Fatalf("Expected err to be of type errInvalid, found: %T", err)
|
2020-03-25 01:40:03 -04:00
|
|
|
}
|
2020-03-31 00:19:18 -04:00
|
|
|
errInv := err.(errInvalid)
|
|
|
|
if len(errInv) != 1 || errInv[0] != someError {
|
2020-03-25 01:40:03 -04:00
|
|
|
t.Errorf("Expected ErrOrNil to return itself, found: %v", err)
|
|
|
|
}
|
|
|
|
}
|