mirror of
https://github.com/aclindsa/ofxgo.git
synced 2025-11-03 01:53:26 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ad638c7e2 | ||
|
|
f19189de45 | ||
|
|
677a09295a | ||
|
|
f75592381a | ||
| ebf7f5b757 | |||
|
|
212fdc731b | ||
|
|
66dd37781f | ||
|
|
67e527c855 | ||
| f41286cac7 | |||
| 423d460747 | |||
| 3e8a9c5a53 | |||
| 35c7116654 | |||
| 286e619071 | |||
| 9dd9c3bd3f | |||
|
|
0f6ceccd86 | ||
| 7691881132 | |||
| 61262b87d8 | |||
| 5e2e3a2bf7 | |||
| 22a6d65b98 | |||
| 77b154695f | |||
| ac09538ec3 | |||
| d8491bed1d | |||
| 1b4f27b31f | |||
| eb35a26986 | |||
| 5c10ac5ea1 | |||
| 94a77ac754 | |||
| de58d3fc0d | |||
| 88e5521348 | |||
| 54666608a4 | |||
| 1cc508c6d3 | |||
| c6a806399a | |||
|
|
2a92b29a62 | ||
|
|
2fbb276a22 | ||
| 06de7e2af6 | |||
| 977dacfbbd | |||
| ddc674b287 | |||
| 29fc9c20fe |
@@ -5,12 +5,15 @@ os:
|
||||
|
||||
go:
|
||||
- 1.9.x
|
||||
- 1.12.x
|
||||
- master
|
||||
|
||||
script:
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
before_install:
|
||||
# Fetch/build coverage reporting tools
|
||||
- go get github.com/mattn/goveralls
|
||||
- go install github.com/mattn/goveralls
|
||||
|
||||
script:
|
||||
- go test -v -covermode=count -coverprofile=coverage.out
|
||||
|
||||
after_script:
|
||||
|
||||
56
README.md
56
README.md
@@ -38,6 +38,62 @@ repository.
|
||||
Documentation can be found with the `go doc` tool, or at
|
||||
https://godoc.org/github.com/aclindsa/ofxgo
|
||||
|
||||
## Example Usage
|
||||
|
||||
The following code snippet demonstrates how to use OFXGo to query and parse
|
||||
OFX code from a checking account, printing the balance and returned transactions:
|
||||
|
||||
```go
|
||||
client := ofxgo.BasicClient{} // Accept the default Client settings
|
||||
|
||||
// These values are specific to your bank
|
||||
var query ofxgo.Request
|
||||
query.URL = "https://secu.example.com/ofx"
|
||||
query.Signon.Org = ofxgo.String("SECU")
|
||||
query.Signon.Fid = ofxgo.String("1234")
|
||||
|
||||
// Set your username/password
|
||||
query.Signon.UserID = ofxgo.String("username")
|
||||
query.Signon.UserPass = ofxgo.String("hunter2")
|
||||
|
||||
uid, _ := ofxgo.RandomUID() // Handle error in real code
|
||||
query.Bank = append(query.Bank, &ofxgo.StatementRequest{
|
||||
TrnUID: *uid,
|
||||
BankAcctFrom: ofxgo.BankAcct{
|
||||
BankID: ofxgo.String("123456789"), // Possibly your routing number
|
||||
AcctID: ofxgo.String("00011122233"), // Possibly your account number
|
||||
AcctType: ofxgo.AcctTypeChecking,
|
||||
},
|
||||
Include: true, // Include transactions (instead of only balance information)
|
||||
})
|
||||
|
||||
response, _ := client.Request(&query) // Handle error in real code
|
||||
|
||||
// Was there an OFX error while processing our request?
|
||||
if response.Signon.Status.Code != 0 {
|
||||
meaning, _ := response.Signon.Status.CodeMeaning()
|
||||
fmt.Printf("Nonzero signon status (%d: %s) with message: %s\n", response.Signon.Status.Code, meaning, response.Signon.Status.Message)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(response.Bank) < 1 {
|
||||
fmt.Println("No banking messages received")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if stmt, ok := response.Bank[0].(*ofxgo.StatementResponse); ok {
|
||||
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
||||
fmt.Println("Transactions:")
|
||||
for _, tran := range stmt.BankTranList.Transactions {
|
||||
currency := stmt.CurDef
|
||||
if ok, _ := tran.Currency.Valid(); ok {
|
||||
currency = tran.Currency.CurSym
|
||||
}
|
||||
fmt.Printf("%s %-15s %-11s %s%s%s\n", tran.DtPosted, tran.TrnAmt.String()+" "+currency.String(), tran.TrnType, tran.Name, tran.Payee.Name, tran.Memo)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
OFXGo requires go >= 1.9
|
||||
|
||||
13
bank.go
13
bank.go
@@ -126,8 +126,8 @@ type Transaction struct {
|
||||
ImageData []ImageData `xml:"IMAGEDATA,omitempty"`
|
||||
|
||||
// Only one of Currency and OrigCurrency can ever be Valid() for the same transaction
|
||||
Currency Currency `xml:"CURRENCY,omitempty"` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
|
||||
OrigCurrency Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
|
||||
Currency *Currency `xml:"CURRENCY,omitempty"` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
|
||||
OrigCurrency *Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
|
||||
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST (Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST.)
|
||||
}
|
||||
|
||||
@@ -166,8 +166,13 @@ func (t Transaction) Valid(version ofxVersion) (bool, error) {
|
||||
} else if len(t.ImageData) > 2 {
|
||||
return false, errors.New("Only 2 of ImageData allowed in Transaction")
|
||||
}
|
||||
ok1, _ := t.Currency.Valid()
|
||||
ok2, _ := t.OrigCurrency.Valid()
|
||||
var ok1, ok2 bool
|
||||
if t.Currency != nil {
|
||||
ok1, _ = t.Currency.Valid()
|
||||
}
|
||||
if t.OrigCurrency != nil {
|
||||
ok2, _ = t.OrigCurrency.Valid()
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
return false, errors.New("Currency and OrigCurrency both supplied for Pending Transaction, only one allowed")
|
||||
}
|
||||
|
||||
106
bank_test.go
106
bank_test.go
@@ -1,7 +1,6 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -42,24 +41,24 @@ func TestMarshalBankStatementRequest(t *testing.T) {
|
||||
</BANKMSGSRQV1>
|
||||
</OFX>`
|
||||
|
||||
var client = ofxgo.Client{
|
||||
var client = BasicClient{
|
||||
AppID: "OFXGO",
|
||||
AppVer: "0001",
|
||||
SpecVersion: ofxgo.OfxVersion203,
|
||||
SpecVersion: OfxVersion203,
|
||||
}
|
||||
|
||||
var request ofxgo.Request
|
||||
var request Request
|
||||
request.Signon.UserID = "myusername"
|
||||
request.Signon.UserPass = "Pa$$word"
|
||||
request.Signon.Org = "BNK"
|
||||
request.Signon.Fid = "1987"
|
||||
|
||||
statementRequest := ofxgo.StatementRequest{
|
||||
statementRequest := StatementRequest{
|
||||
TrnUID: "123",
|
||||
BankAcctFrom: ofxgo.BankAcct{
|
||||
BankAcctFrom: BankAcct{
|
||||
BankID: "318398732",
|
||||
AcctID: "78346129",
|
||||
AcctType: ofxgo.AcctTypeChecking,
|
||||
AcctType: AcctTypeChecking,
|
||||
},
|
||||
Include: true,
|
||||
}
|
||||
@@ -68,7 +67,7 @@ func TestMarshalBankStatementRequest(t *testing.T) {
|
||||
request.SetClientFields(&client)
|
||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
request.Signon.DtClient = *ofxgo.NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
||||
request.Signon.DtClient = *NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
||||
|
||||
marshalCheckRequest(t, &request, expectedString)
|
||||
}
|
||||
@@ -87,53 +86,53 @@ NEWFILEUID:NONE
|
||||
<OFX>
|
||||
<SIGNONMSGSRQV1>
|
||||
<SONRQ>
|
||||
<DTCLIENT>20060115112300.000[-5:EST]</DTCLIENT>
|
||||
<USERID>myusername</USERID>
|
||||
<USERPASS>Pa$$word</USERPASS>
|
||||
<LANGUAGE>ENG</LANGUAGE>
|
||||
<DTCLIENT>20060115112300.000[-5:EST]
|
||||
<USERID>myusername
|
||||
<USERPASS>Pa$$word
|
||||
<LANGUAGE>ENG
|
||||
<FI>
|
||||
<ORG>BNK</ORG>
|
||||
<FID>1987</FID>
|
||||
<ORG>BNK
|
||||
<FID>1987
|
||||
</FI>
|
||||
<APPID>OFXGO</APPID>
|
||||
<APPVER>0001</APPVER>
|
||||
<APPID>OFXGO
|
||||
<APPVER>0001
|
||||
</SONRQ>
|
||||
</SIGNONMSGSRQV1>
|
||||
<BANKMSGSRQV1>
|
||||
<STMTTRNRQ>
|
||||
<TRNUID>123</TRNUID>
|
||||
<TRNUID>123
|
||||
<STMTRQ>
|
||||
<BANKACCTFROM>
|
||||
<BANKID>318398732</BANKID>
|
||||
<ACCTID>78346129</ACCTID>
|
||||
<ACCTTYPE>CHECKING</ACCTTYPE>
|
||||
<BANKID>318398732
|
||||
<ACCTID>78346129
|
||||
<ACCTTYPE>CHECKING
|
||||
</BANKACCTFROM>
|
||||
<INCTRAN>
|
||||
<INCLUDE>Y</INCLUDE>
|
||||
<INCLUDE>Y
|
||||
</INCTRAN>
|
||||
</STMTRQ>
|
||||
</STMTTRNRQ>
|
||||
</BANKMSGSRQV1>
|
||||
</OFX>`
|
||||
|
||||
var client = ofxgo.Client{
|
||||
var client = BasicClient{
|
||||
AppID: "OFXGO",
|
||||
AppVer: "0001",
|
||||
SpecVersion: ofxgo.OfxVersion103,
|
||||
SpecVersion: OfxVersion103,
|
||||
}
|
||||
|
||||
var request ofxgo.Request
|
||||
var request Request
|
||||
request.Signon.UserID = "myusername"
|
||||
request.Signon.UserPass = "Pa$$word"
|
||||
request.Signon.Org = "BNK"
|
||||
request.Signon.Fid = "1987"
|
||||
|
||||
statementRequest := ofxgo.StatementRequest{
|
||||
statementRequest := StatementRequest{
|
||||
TrnUID: "123",
|
||||
BankAcctFrom: ofxgo.BankAcct{
|
||||
BankAcctFrom: BankAcct{
|
||||
BankID: "318398732",
|
||||
AcctID: "78346129",
|
||||
AcctType: ofxgo.AcctTypeChecking,
|
||||
AcctType: AcctTypeChecking,
|
||||
},
|
||||
Include: true,
|
||||
}
|
||||
@@ -142,7 +141,7 @@ NEWFILEUID:NONE
|
||||
request.SetClientFields(&client)
|
||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
request.Signon.DtClient = *ofxgo.NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
||||
request.Signon.DtClient = *NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
||||
|
||||
marshalCheckRequest(t, &request, expectedString)
|
||||
}
|
||||
@@ -211,76 +210,77 @@ func TestUnmarshalBankStatementResponse(t *testing.T) {
|
||||
</STMTTRNRS>
|
||||
</BANKMSGSRSV1>
|
||||
</OFX>`)
|
||||
var expected ofxgo.Response
|
||||
var expected Response
|
||||
|
||||
expected.Version = ofxgo.OfxVersion203
|
||||
expected.Version = OfxVersion203
|
||||
expected.Signon.Status.Code = 0
|
||||
expected.Signon.Status.Severity = "INFO"
|
||||
expected.Signon.DtServer = *ofxgo.NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
||||
expected.Signon.DtServer = *NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
||||
expected.Signon.Language = "ENG"
|
||||
expected.Signon.DtProfUp = ofxgo.NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
||||
expected.Signon.DtAcctUp = ofxgo.NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
||||
expected.Signon.DtProfUp = NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
||||
expected.Signon.DtAcctUp = NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
||||
expected.Signon.Org = "BNK"
|
||||
expected.Signon.Fid = "1987"
|
||||
|
||||
var trnamt1, trnamt2 ofxgo.Amount
|
||||
var trnamt1, trnamt2 Amount
|
||||
trnamt1.SetFrac64(-20000, 100)
|
||||
trnamt2.SetFrac64(-30000, 100)
|
||||
|
||||
banktranlist := ofxgo.TransactionList{
|
||||
DtStart: *ofxgo.NewDateGMT(2006, 1, 1, 0, 0, 0, 0),
|
||||
DtEnd: *ofxgo.NewDateGMT(2006, 1, 15, 0, 0, 0, 0),
|
||||
Transactions: []ofxgo.Transaction{
|
||||
banktranlist := TransactionList{
|
||||
DtStart: *NewDateGMT(2006, 1, 1, 0, 0, 0, 0),
|
||||
DtEnd: *NewDateGMT(2006, 1, 15, 0, 0, 0, 0),
|
||||
Transactions: []Transaction{
|
||||
{
|
||||
TrnType: ofxgo.TrnTypeCheck,
|
||||
DtPosted: *ofxgo.NewDateGMT(2006, 1, 4, 0, 0, 0, 0),
|
||||
TrnType: TrnTypeCheck,
|
||||
DtPosted: *NewDateGMT(2006, 1, 4, 0, 0, 0, 0),
|
||||
TrnAmt: trnamt1,
|
||||
FiTID: "00592",
|
||||
CheckNum: "2002",
|
||||
},
|
||||
{
|
||||
TrnType: ofxgo.TrnTypeATM,
|
||||
DtPosted: *ofxgo.NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
||||
DtUser: ofxgo.NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
||||
TrnType: TrnTypeATM,
|
||||
DtPosted: *NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
||||
DtUser: NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
||||
TrnAmt: trnamt2,
|
||||
FiTID: "00679",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var balamt, availbalamt ofxgo.Amount
|
||||
var balamt, availbalamt Amount
|
||||
balamt.SetFrac64(20029, 100)
|
||||
availbalamt.SetFrac64(20029, 100)
|
||||
|
||||
usd, err := ofxgo.NewCurrSymbol("USD")
|
||||
usd, err := NewCurrSymbol("USD")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating CurrSymbol for USD\n")
|
||||
}
|
||||
|
||||
statementResponse := ofxgo.StatementResponse{
|
||||
statementResponse := StatementResponse{
|
||||
TrnUID: "1001",
|
||||
Status: ofxgo.Status{
|
||||
Status: Status{
|
||||
Code: 0,
|
||||
Severity: "INFO",
|
||||
},
|
||||
CurDef: *usd,
|
||||
BankAcctFrom: ofxgo.BankAcct{
|
||||
BankAcctFrom: BankAcct{
|
||||
BankID: "318398732",
|
||||
AcctID: "78346129",
|
||||
AcctType: ofxgo.AcctTypeChecking,
|
||||
AcctType: AcctTypeChecking,
|
||||
},
|
||||
BankTranList: &banktranlist,
|
||||
BalAmt: balamt,
|
||||
DtAsOf: *ofxgo.NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
||||
DtAsOf: *NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
||||
AvailBalAmt: &availbalamt,
|
||||
AvailDtAsOf: ofxgo.NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
||||
AvailDtAsOf: NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
||||
}
|
||||
expected.Bank = append(expected.Bank, &statementResponse)
|
||||
|
||||
response, err := ofxgo.ParseResponse(responseReader)
|
||||
response, err := ParseResponse(responseReader)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||
}
|
||||
|
||||
checkResponsesEqual(t, &expected, response)
|
||||
checkResponseRoundTrip(t, response)
|
||||
}
|
||||
|
||||
87
basic_client.go
Normal file
87
basic_client.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BasicClient provides a standard Client implementation suitable for most
|
||||
// financial institutions. BasicClient uses default, non-zero settings, even if
|
||||
// its fields are not initialized.
|
||||
type BasicClient struct {
|
||||
// Request fields to overwrite with the client's values. If nonempty,
|
||||
// defaults are used
|
||||
SpecVersion ofxVersion // VERSION in header
|
||||
AppID string // SONRQ>APPID
|
||||
AppVer string // SONRQ>APPVER
|
||||
|
||||
// Don't insert newlines or indentation when marshalling to SGML/XML
|
||||
NoIndent bool
|
||||
// Use carriage returns on new lines
|
||||
CarriageReturn bool
|
||||
}
|
||||
|
||||
// OfxVersion returns the OFX specification version this BasicClient will marshal
|
||||
// Requests as. Defaults to "203" if the client's SpecVersion field is empty.
|
||||
func (c *BasicClient) OfxVersion() ofxVersion {
|
||||
if c.SpecVersion.Valid() {
|
||||
return c.SpecVersion
|
||||
}
|
||||
return OfxVersion203
|
||||
}
|
||||
|
||||
// ID returns this BasicClient's OFX AppID field, defaulting to "OFXGO" if
|
||||
// unspecified.
|
||||
func (c *BasicClient) ID() String {
|
||||
if len(c.AppID) > 0 {
|
||||
return String(c.AppID)
|
||||
}
|
||||
return String("OFXGO")
|
||||
}
|
||||
|
||||
// Version returns this BasicClient's version number as a string, defaulting to
|
||||
// "0001" if unspecified.
|
||||
func (c *BasicClient) Version() String {
|
||||
if len(c.AppVer) > 0 {
|
||||
return String(c.AppVer)
|
||||
}
|
||||
return String("0001")
|
||||
}
|
||||
|
||||
// IndentRequests returns true if the marshaled XML should be indented (and
|
||||
// contain newlines, since the two are linked in the current implementation)
|
||||
func (c *BasicClient) IndentRequests() bool {
|
||||
return !c.NoIndent
|
||||
}
|
||||
|
||||
// CarriageReturnNewLines returns true if carriage returns should be used on new lines, false otherwise
|
||||
func (c *BasicClient) CarriageReturnNewLines() bool {
|
||||
return c.CarriageReturn
|
||||
}
|
||||
|
||||
func (c *BasicClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
||||
if !strings.HasPrefix(URL, "https://") {
|
||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||
}
|
||||
|
||||
response, err := http.Post(URL, "application/x-ofx", r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||
return clientRequestNoParse(c, r)
|
||||
}
|
||||
|
||||
func (c *BasicClient) Request(r *Request) (*Response, error) {
|
||||
return clientRequest(c, r)
|
||||
}
|
||||
202
client.go
202
client.go
@@ -1,127 +1,83 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Client serves to aggregate OFX client settings that may be necessary to talk
|
||||
// to a particular server due to quirks in that server's implementation. Client
|
||||
// also provides the Request, RequestNoParse, and RawRequest helper methods to
|
||||
// aid in making and parsing requests. Client uses default, non-zero settings,
|
||||
// even if its fields are not initialized.
|
||||
type Client struct {
|
||||
// Request fields to overwrite with the client's values. If nonempty,
|
||||
// defaults are used
|
||||
SpecVersion ofxVersion // VERSION in header
|
||||
AppID string // SONRQ>APPID
|
||||
AppVer string // SONRQ>APPVER
|
||||
// to a particular server due to quirks in that server's implementation.
|
||||
// Client also provides the Request and RequestNoParse helper methods to aid in
|
||||
// making and parsing requests.
|
||||
type Client interface {
|
||||
// Used to fill out a Request object
|
||||
OfxVersion() ofxVersion
|
||||
ID() String
|
||||
Version() String
|
||||
IndentRequests() bool
|
||||
CarriageReturnNewLines() bool
|
||||
|
||||
// Don't insert newlines or indentation when marshalling to SGML/XML
|
||||
NoIndent bool
|
||||
// Request marshals a Request object into XML, makes an HTTP request
|
||||
// against it's URL, and then unmarshals the response into a Response
|
||||
// object.
|
||||
//
|
||||
// Before being marshaled, some of the the Request object's values are
|
||||
// overwritten, namely those dictated by the BasicClient's configuration
|
||||
// (Version, AppID, AppVer fields), and the client's current time
|
||||
// (DtClient). These are updated in place in the supplied Request object so
|
||||
// they may later be inspected by the caller.
|
||||
Request(r *Request) (*Response, error)
|
||||
|
||||
// RequestNoParse marshals a Request object into XML, makes an HTTP
|
||||
// request, and returns the raw HTTP response. Unlike RawRequest(), it
|
||||
// takes client settings into account. Unlike Request(), it doesn't parse
|
||||
// the response into an ofxgo.Request object.
|
||||
//
|
||||
// Caveat: The caller is responsible for closing the http Response.Body
|
||||
// (see the http module's documentation for more information)
|
||||
RequestNoParse(r *Request) (*http.Response, error)
|
||||
|
||||
// RawRequest is little more than a thin wrapper around http.Post
|
||||
//
|
||||
// In most cases, you should probably be using Request() instead, but
|
||||
// RawRequest can be useful if you need to read the raw unparsed http
|
||||
// response yourself (perhaps for downloading an OFX file for use by an
|
||||
// external program, or debugging server behavior), or have a handcrafted
|
||||
// request you'd like to try.
|
||||
//
|
||||
// Caveats: RawRequest does *not* take client settings into account as
|
||||
// Client.Request() does, so your particular server may or may not like
|
||||
// whatever we read from 'r'. The caller is responsible for closing the
|
||||
// http Response.Body (see the http module's documentation for more
|
||||
// information)
|
||||
RawRequest(URL string, r io.Reader) (*http.Response, error)
|
||||
}
|
||||
|
||||
var defaultClient Client
|
||||
type clientCreationFunc func(*BasicClient) Client
|
||||
|
||||
// OfxVersion returns the OFX specification version this Client will marshal
|
||||
// Requests as. Defaults to "203" if the client's SpecVersion field is empty.
|
||||
func (c *Client) OfxVersion() ofxVersion {
|
||||
if c.SpecVersion.Valid() {
|
||||
return c.SpecVersion
|
||||
// GetClient returns a new Client for a given URL. It attempts to find a
|
||||
// specialized client for this URL, but simply returns the passed-in
|
||||
// BasicClient if no such match is found.
|
||||
func GetClient(URL string, bc *BasicClient) Client {
|
||||
clients := []struct {
|
||||
URL string
|
||||
Func clientCreationFunc
|
||||
}{
|
||||
{"https://ofx.discovercard.com", NewDiscoverCardClient},
|
||||
{"https://vesnc.vanguard.com/us/OfxDirectConnectServlet", NewVanguardClient},
|
||||
}
|
||||
return OfxVersion203
|
||||
for _, client := range clients {
|
||||
if client.URL == strings.Trim(URL, "/") {
|
||||
return client.Func(bc)
|
||||
}
|
||||
}
|
||||
return bc
|
||||
}
|
||||
|
||||
// ID returns this Client's OFX AppID field, defaulting to "OFXGO" if
|
||||
// unspecified.
|
||||
func (c *Client) ID() String {
|
||||
if len(c.AppID) > 0 {
|
||||
return String(c.AppID)
|
||||
}
|
||||
return String("OFXGO")
|
||||
}
|
||||
|
||||
// Version returns this Client's version number as a string, defaulting to
|
||||
// "0001" if unspecified.
|
||||
func (c *Client) Version() String {
|
||||
if len(c.AppVer) > 0 {
|
||||
return String(c.AppVer)
|
||||
}
|
||||
return String("0001")
|
||||
}
|
||||
|
||||
// IndentRequests returns true if the marshaled XML should be indented (and
|
||||
// contain newlines, since the two are linked in the current implementation)
|
||||
func (c *Client) IndentRequests() bool {
|
||||
return !c.NoIndent
|
||||
}
|
||||
|
||||
// RawRequest is little more than a thin wrapper around http.Post
|
||||
//
|
||||
// In most cases, you should probably be using Request() instead, but
|
||||
// RawRequest can be useful if you need to read the raw unparsed http response
|
||||
// yourself (perhaps for downloading an OFX file for use by an external
|
||||
// program, or debugging server behavior), or have a handcrafted request you'd
|
||||
// like to try.
|
||||
//
|
||||
// Caveats: RawRequest does *not* take client settings into account as
|
||||
// Request() does, so your particular server may or may not like whatever we
|
||||
// read from 'r'. The caller is responsible for closing the http Response.Body
|
||||
// (see the http module's documentation for more information)
|
||||
func RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
||||
if !strings.HasPrefix(URL, "https://") {
|
||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||
}
|
||||
|
||||
response, err := http.Post(URL, "application/x-ofx", r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RawRequestCookies is RawRequest with the added feature of sending cookies
|
||||
func RawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
|
||||
if !strings.HasPrefix(URL, "https://") {
|
||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", URL, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/x-ofx")
|
||||
for _, cookie := range cookies {
|
||||
request.AddCookie(cookie)
|
||||
}
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RequestNoParse marshals a Request object into XML, makes an HTTP request,
|
||||
// and returns the raw HTTP response. Unlike RawRequest(), it takes client
|
||||
// settings into account. Unlike Request(), it doesn't parse the response into
|
||||
// an ofxgo.Request object.
|
||||
//
|
||||
// Caveat: The caller is responsible for closing the http Response.Body (see
|
||||
// the http module's documentation for more information)
|
||||
func (c *Client) RequestNoParse(r *Request) (*http.Response, error) {
|
||||
// clientRequestNoParse can be used for building clients' RequestNoParse
|
||||
// methods if they require fairly standard behavior
|
||||
func clientRequestNoParse(c Client, r *Request) (*http.Response, error) {
|
||||
r.SetClientFields(c)
|
||||
|
||||
b, err := r.Marshal()
|
||||
@@ -129,34 +85,12 @@ func (c *Client) RequestNoParse(r *Request) (*http.Response, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := RawRequest(r.URL, b)
|
||||
|
||||
// Some financial institutions (cough, Vanguard, cough), require a cookie
|
||||
// to be set on the http request, or they return empty responses.
|
||||
// Fortunately, the initial response contains the cookie we need, so if we
|
||||
// detect an empty response with cookies set that didn't have any errors,
|
||||
// re-try the request while sending their cookies back to them.
|
||||
if err == nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
||||
b, err = r.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return RawRequestCookies(r.URL, b, response.Cookies())
|
||||
}
|
||||
|
||||
return response, err
|
||||
return c.RawRequest(r.URL, b)
|
||||
}
|
||||
|
||||
// Request marshals a Request object into XML, makes an HTTP request against
|
||||
// it's URL, and then unmarshals the response into a Response object.
|
||||
//
|
||||
// Before being marshaled, some of the the Request object's values are
|
||||
// overwritten, namely those dictated by the Client's configuration (Version,
|
||||
// AppID, AppVer fields), and the client's curren time (DtClient). These are
|
||||
// updated in place in the supplied Request object so they may later be
|
||||
// inspected by the caller.
|
||||
func (c *Client) Request(r *Request) (*Response, error) {
|
||||
// clientRequest can be used for building clients' Request methods if they
|
||||
// require fairly standard behavior
|
||||
func clientRequest(c Client, r *Request) (*Response, error) {
|
||||
response, err := c.RequestNoParse(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -43,6 +43,8 @@ var appVersions = map[string][]string{
|
||||
"0001",
|
||||
},
|
||||
"QWIN": { // Intuit Quicken Windows
|
||||
"2600", // 2017
|
||||
"2500", // 2016
|
||||
"2400", // 2015
|
||||
"2300", // 2014
|
||||
"2200", // 2013
|
||||
@@ -126,12 +128,13 @@ func tryProfile(appID, appVer, version string, noindent bool) bool {
|
||||
fmt.Println("Error creating new OfxVersion enum:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var client = ofxgo.Client{
|
||||
AppID: appID,
|
||||
AppVer: appVer,
|
||||
SpecVersion: ver,
|
||||
NoIndent: noindent,
|
||||
}
|
||||
var client = ofxgo.GetClient(serverURL,
|
||||
&ofxgo.BasicClient{
|
||||
AppID: appID,
|
||||
AppVer: appVer,
|
||||
SpecVersion: ver,
|
||||
NoIndent: noindent,
|
||||
})
|
||||
|
||||
var query ofxgo.Request
|
||||
query.URL = serverURL
|
||||
|
||||
@@ -6,18 +6,19 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func newRequest() (*ofxgo.Client, *ofxgo.Request) {
|
||||
func newRequest() (ofxgo.Client, *ofxgo.Request) {
|
||||
ver, err := ofxgo.NewOfxVersion(ofxVersion)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating new OfxVersion enum:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var client = ofxgo.Client{
|
||||
AppID: appID,
|
||||
AppVer: appVer,
|
||||
SpecVersion: ver,
|
||||
NoIndent: noIndentRequests,
|
||||
}
|
||||
var client = ofxgo.GetClient(serverURL,
|
||||
&ofxgo.BasicClient{
|
||||
AppID: appID,
|
||||
AppVer: appVer,
|
||||
SpecVersion: ver,
|
||||
NoIndent: noIndentRequests,
|
||||
})
|
||||
|
||||
var query ofxgo.Request
|
||||
query.URL = serverURL
|
||||
@@ -27,5 +28,5 @@ func newRequest() (*ofxgo.Client, *ofxgo.Request) {
|
||||
query.Signon.Org = ofxgo.String(org)
|
||||
query.Signon.Fid = ofxgo.String(fid)
|
||||
|
||||
return &client, &query
|
||||
return client, &query
|
||||
}
|
||||
|
||||
40
common.go
40
common.go
@@ -3,10 +3,50 @@ package ofxgo
|
||||
//go:generate ./generate_constants.py
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
|
||||
func writeHeader(b *bytes.Buffer, v ofxVersion, carriageReturn bool) error {
|
||||
// Write the header appropriate to our version
|
||||
switch v {
|
||||
case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160:
|
||||
header := `OFXHEADER:100
|
||||
DATA:OFXSGML
|
||||
VERSION:` + v.String() + `
|
||||
SECURITY:NONE
|
||||
ENCODING:USASCII
|
||||
CHARSET:1252
|
||||
COMPRESSION:NONE
|
||||
OLDFILEUID:NONE
|
||||
NEWFILEUID:NONE
|
||||
|
||||
`
|
||||
if carriageReturn {
|
||||
header = strings.Replace(header, "\n", "\r\n", -1)
|
||||
}
|
||||
b.WriteString(header)
|
||||
case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220:
|
||||
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>`)
|
||||
if carriageReturn {
|
||||
b.WriteByte('\r')
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + v.String() + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>`)
|
||||
if carriageReturn {
|
||||
b.WriteByte('\r')
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
default:
|
||||
return fmt.Errorf("%d is not a valid OFX version string", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Message represents an OFX message in a message set. it is used to ease
|
||||
// marshalling and unmarshalling.
|
||||
type Message interface {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStatusValid(t *testing.T) {
|
||||
s := ofxgo.Status{
|
||||
s := Status{
|
||||
Code: 0,
|
||||
Severity: "INFO",
|
||||
Message: "Success",
|
||||
@@ -32,7 +31,7 @@ func TestStatusValid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStatusCodeMeaning(t *testing.T) {
|
||||
s := ofxgo.Status{
|
||||
s := Status{
|
||||
Code: 15500,
|
||||
Severity: "ERROR",
|
||||
}
|
||||
@@ -51,7 +50,7 @@ func TestStatusCodeMeaning(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStatusCodeConditions(t *testing.T) {
|
||||
s := ofxgo.Status{
|
||||
s := Status{
|
||||
Code: 2006,
|
||||
Severity: "ERROR",
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ package ofxgo
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/aclindsa/xml"
|
||||
"strings"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
|
||||
type ofxVersion uint
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
/*
|
||||
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
|
||||
@@ -7,14 +7,14 @@ package ofxgo_test
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/xml"
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
|
||||
func TestOfxVersion(t *testing.T) {
|
||||
e, err := ofxgo.NewOfxVersion("102")
|
||||
e, err := NewOfxVersion("102")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new OfxVersion from string \"102\"\n")
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func TestOfxVersion(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "220", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewOfxVersion("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewOfxVersion("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new OfxVersion from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func TestOfxVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAcctType(t *testing.T) {
|
||||
e, err := ofxgo.NewAcctType("CHECKING")
|
||||
e, err := NewAcctType("CHECKING")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new AcctType from string \"CHECKING\"\n")
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func TestAcctType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "CD", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new AcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func TestAcctType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrnType(t *testing.T) {
|
||||
e, err := ofxgo.NewTrnType("CREDIT")
|
||||
e, err := NewTrnType("CREDIT")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new TrnType from string \"CREDIT\"\n")
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func TestTrnType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewTrnType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewTrnType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new TrnType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestTrnType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImageType(t *testing.T) {
|
||||
e, err := ofxgo.NewImageType("STATEMENT")
|
||||
e, err := NewImageType("STATEMENT")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new ImageType from string \"STATEMENT\"\n")
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func TestImageType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "TAX", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewImageType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewImageType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new ImageType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func TestImageType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImageRefType(t *testing.T) {
|
||||
e, err := ofxgo.NewImageRefType("OPAQUE")
|
||||
e, err := NewImageRefType("OPAQUE")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new ImageRefType from string \"OPAQUE\"\n")
|
||||
}
|
||||
@@ -211,7 +211,7 @@ func TestImageRefType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "FORMURL", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewImageRefType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewImageRefType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new ImageRefType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -239,7 +239,7 @@ func TestImageRefType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCheckSup(t *testing.T) {
|
||||
e, err := ofxgo.NewCheckSup("FRONTONLY")
|
||||
e, err := NewCheckSup("FRONTONLY")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new CheckSup from string \"FRONTONLY\"\n")
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func TestCheckSup(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "FRONTANDBACK", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewCheckSup("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewCheckSup("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new CheckSup from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -284,7 +284,7 @@ func TestCheckSup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCorrectAction(t *testing.T) {
|
||||
e, err := ofxgo.NewCorrectAction("DELETE")
|
||||
e, err := NewCorrectAction("DELETE")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new CorrectAction from string \"DELETE\"\n")
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func TestCorrectAction(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "REPLACE", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewCorrectAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewCorrectAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new CorrectAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -329,7 +329,7 @@ func TestCorrectAction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBalType(t *testing.T) {
|
||||
e, err := ofxgo.NewBalType("DOLLAR")
|
||||
e, err := NewBalType("DOLLAR")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new BalType from string \"DOLLAR\"\n")
|
||||
}
|
||||
@@ -346,7 +346,7 @@ func TestBalType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "NUMBER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewBalType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewBalType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new BalType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -374,7 +374,7 @@ func TestBalType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInv401kSource(t *testing.T) {
|
||||
e, err := ofxgo.NewInv401kSource("PRETAX")
|
||||
e, err := NewInv401kSource("PRETAX")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new Inv401kSource from string \"PRETAX\"\n")
|
||||
}
|
||||
@@ -391,7 +391,7 @@ func TestInv401kSource(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHERNONVEST", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewInv401kSource("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewInv401kSource("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new Inv401kSource from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -419,7 +419,7 @@ func TestInv401kSource(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSubAcctType(t *testing.T) {
|
||||
e, err := ofxgo.NewSubAcctType("CASH")
|
||||
e, err := NewSubAcctType("CASH")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new SubAcctType from string \"CASH\"\n")
|
||||
}
|
||||
@@ -436,7 +436,7 @@ func TestSubAcctType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewSubAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewSubAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new SubAcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -464,7 +464,7 @@ func TestSubAcctType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuyType(t *testing.T) {
|
||||
e, err := ofxgo.NewBuyType("BUY")
|
||||
e, err := NewBuyType("BUY")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new BuyType from string \"BUY\"\n")
|
||||
}
|
||||
@@ -481,7 +481,7 @@ func TestBuyType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "BUYTOCOVER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new BuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -509,7 +509,7 @@ func TestBuyType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOptAction(t *testing.T) {
|
||||
e, err := ofxgo.NewOptAction("EXERCISE")
|
||||
e, err := NewOptAction("EXERCISE")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new OptAction from string \"EXERCISE\"\n")
|
||||
}
|
||||
@@ -526,7 +526,7 @@ func TestOptAction(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "EXPIRE", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewOptAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewOptAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new OptAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -554,7 +554,7 @@ func TestOptAction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTferAction(t *testing.T) {
|
||||
e, err := ofxgo.NewTferAction("IN")
|
||||
e, err := NewTferAction("IN")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new TferAction from string \"IN\"\n")
|
||||
}
|
||||
@@ -571,7 +571,7 @@ func TestTferAction(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OUT", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewTferAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewTferAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new TferAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -599,7 +599,7 @@ func TestTferAction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPosType(t *testing.T) {
|
||||
e, err := ofxgo.NewPosType("LONG")
|
||||
e, err := NewPosType("LONG")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new PosType from string \"LONG\"\n")
|
||||
}
|
||||
@@ -616,7 +616,7 @@ func TestPosType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "SHORT", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewPosType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewPosType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new PosType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -644,7 +644,7 @@ func TestPosType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSecured(t *testing.T) {
|
||||
e, err := ofxgo.NewSecured("NAKED")
|
||||
e, err := NewSecured("NAKED")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new Secured from string \"NAKED\"\n")
|
||||
}
|
||||
@@ -661,7 +661,7 @@ func TestSecured(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "COVERED", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewSecured("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewSecured("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new Secured from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -689,7 +689,7 @@ func TestSecured(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDuration(t *testing.T) {
|
||||
e, err := ofxgo.NewDuration("DAY")
|
||||
e, err := NewDuration("DAY")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new Duration from string \"DAY\"\n")
|
||||
}
|
||||
@@ -706,7 +706,7 @@ func TestDuration(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "IMMEDIATE", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewDuration("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewDuration("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new Duration from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -734,7 +734,7 @@ func TestDuration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRestriction(t *testing.T) {
|
||||
e, err := ofxgo.NewRestriction("ALLORNONE")
|
||||
e, err := NewRestriction("ALLORNONE")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new Restriction from string \"ALLORNONE\"\n")
|
||||
}
|
||||
@@ -751,7 +751,7 @@ func TestRestriction(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "NONE", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewRestriction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewRestriction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new Restriction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -779,7 +779,7 @@ func TestRestriction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnitType(t *testing.T) {
|
||||
e, err := ofxgo.NewUnitType("SHARES")
|
||||
e, err := NewUnitType("SHARES")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new UnitType from string \"SHARES\"\n")
|
||||
}
|
||||
@@ -796,7 +796,7 @@ func TestUnitType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "CURRENCY", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewUnitType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewUnitType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new UnitType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -824,7 +824,7 @@ func TestUnitType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOptBuyType(t *testing.T) {
|
||||
e, err := ofxgo.NewOptBuyType("BUYTOOPEN")
|
||||
e, err := NewOptBuyType("BUYTOOPEN")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new OptBuyType from string \"BUYTOOPEN\"\n")
|
||||
}
|
||||
@@ -841,7 +841,7 @@ func TestOptBuyType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "BUYTOCLOSE", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewOptBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewOptBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new OptBuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -869,7 +869,7 @@ func TestOptBuyType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSellType(t *testing.T) {
|
||||
e, err := ofxgo.NewSellType("SELL")
|
||||
e, err := NewSellType("SELL")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new SellType from string \"SELL\"\n")
|
||||
}
|
||||
@@ -886,7 +886,7 @@ func TestSellType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "SELLSHORT", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new SellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -914,7 +914,7 @@ func TestSellType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoanPmtFreq(t *testing.T) {
|
||||
e, err := ofxgo.NewLoanPmtFreq("WEEKLY")
|
||||
e, err := NewLoanPmtFreq("WEEKLY")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new LoanPmtFreq from string \"WEEKLY\"\n")
|
||||
}
|
||||
@@ -931,7 +931,7 @@ func TestLoanPmtFreq(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewLoanPmtFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewLoanPmtFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new LoanPmtFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -959,7 +959,7 @@ func TestLoanPmtFreq(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIncomeType(t *testing.T) {
|
||||
e, err := ofxgo.NewIncomeType("CGLONG")
|
||||
e, err := NewIncomeType("CGLONG")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new IncomeType from string \"CGLONG\"\n")
|
||||
}
|
||||
@@ -976,7 +976,7 @@ func TestIncomeType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "MISC", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewIncomeType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewIncomeType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new IncomeType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1004,7 +1004,7 @@ func TestIncomeType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSellReason(t *testing.T) {
|
||||
e, err := ofxgo.NewSellReason("CALL")
|
||||
e, err := NewSellReason("CALL")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new SellReason from string \"CALL\"\n")
|
||||
}
|
||||
@@ -1021,7 +1021,7 @@ func TestSellReason(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "MATURITY", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewSellReason("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewSellReason("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new SellReason from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1049,7 +1049,7 @@ func TestSellReason(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOptSellType(t *testing.T) {
|
||||
e, err := ofxgo.NewOptSellType("SELLTOCLOSE")
|
||||
e, err := NewOptSellType("SELLTOCLOSE")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new OptSellType from string \"SELLTOCLOSE\"\n")
|
||||
}
|
||||
@@ -1066,7 +1066,7 @@ func TestOptSellType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "SELLTOOPEN", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewOptSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewOptSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new OptSellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1094,7 +1094,7 @@ func TestOptSellType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRelType(t *testing.T) {
|
||||
e, err := ofxgo.NewRelType("SPREAD")
|
||||
e, err := NewRelType("SPREAD")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new RelType from string \"SPREAD\"\n")
|
||||
}
|
||||
@@ -1111,7 +1111,7 @@ func TestRelType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewRelType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewRelType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new RelType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1139,7 +1139,7 @@ func TestRelType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCharType(t *testing.T) {
|
||||
e, err := ofxgo.NewCharType("ALPHAONLY")
|
||||
e, err := NewCharType("ALPHAONLY")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new CharType from string \"ALPHAONLY\"\n")
|
||||
}
|
||||
@@ -1156,7 +1156,7 @@ func TestCharType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "ALPHAANDNUMERIC", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewCharType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewCharType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new CharType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1184,7 +1184,7 @@ func TestCharType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyncMode(t *testing.T) {
|
||||
e, err := ofxgo.NewSyncMode("FULL")
|
||||
e, err := NewSyncMode("FULL")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new SyncMode from string \"FULL\"\n")
|
||||
}
|
||||
@@ -1201,7 +1201,7 @@ func TestSyncMode(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "LITE", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewSyncMode("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewSyncMode("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new SyncMode from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1229,7 +1229,7 @@ func TestSyncMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOfxSec(t *testing.T) {
|
||||
e, err := ofxgo.NewOfxSec("NONE")
|
||||
e, err := NewOfxSec("NONE")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new OfxSec from string \"NONE\"\n")
|
||||
}
|
||||
@@ -1246,7 +1246,7 @@ func TestOfxSec(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "TYPE 1", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewOfxSec("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewOfxSec("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new OfxSec from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1274,7 +1274,7 @@ func TestOfxSec(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDebtType(t *testing.T) {
|
||||
e, err := ofxgo.NewDebtType("COUPON")
|
||||
e, err := NewDebtType("COUPON")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new DebtType from string \"COUPON\"\n")
|
||||
}
|
||||
@@ -1291,7 +1291,7 @@ func TestDebtType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "ZERO", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewDebtType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewDebtType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new DebtType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1319,7 +1319,7 @@ func TestDebtType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDebtClass(t *testing.T) {
|
||||
e, err := ofxgo.NewDebtClass("TREASURY")
|
||||
e, err := NewDebtClass("TREASURY")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new DebtClass from string \"TREASURY\"\n")
|
||||
}
|
||||
@@ -1336,7 +1336,7 @@ func TestDebtClass(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewDebtClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewDebtClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new DebtClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1364,7 +1364,7 @@ func TestDebtClass(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCouponFreq(t *testing.T) {
|
||||
e, err := ofxgo.NewCouponFreq("MONTHLY")
|
||||
e, err := NewCouponFreq("MONTHLY")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new CouponFreq from string \"MONTHLY\"\n")
|
||||
}
|
||||
@@ -1381,7 +1381,7 @@ func TestCouponFreq(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewCouponFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewCouponFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new CouponFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1409,7 +1409,7 @@ func TestCouponFreq(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCallType(t *testing.T) {
|
||||
e, err := ofxgo.NewCallType("CALL")
|
||||
e, err := NewCallType("CALL")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new CallType from string \"CALL\"\n")
|
||||
}
|
||||
@@ -1426,7 +1426,7 @@ func TestCallType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "MATURITY", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewCallType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewCallType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new CallType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1454,7 +1454,7 @@ func TestCallType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssetClass(t *testing.T) {
|
||||
e, err := ofxgo.NewAssetClass("DOMESTICBOND")
|
||||
e, err := NewAssetClass("DOMESTICBOND")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new AssetClass from string \"DOMESTICBOND\"\n")
|
||||
}
|
||||
@@ -1471,7 +1471,7 @@ func TestAssetClass(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewAssetClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewAssetClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new AssetClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1499,7 +1499,7 @@ func TestAssetClass(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMfType(t *testing.T) {
|
||||
e, err := ofxgo.NewMfType("OPENEND")
|
||||
e, err := NewMfType("OPENEND")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new MfType from string \"OPENEND\"\n")
|
||||
}
|
||||
@@ -1516,7 +1516,7 @@ func TestMfType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewMfType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewMfType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new MfType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1544,7 +1544,7 @@ func TestMfType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOptType(t *testing.T) {
|
||||
e, err := ofxgo.NewOptType("PUT")
|
||||
e, err := NewOptType("PUT")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new OptType from string \"PUT\"\n")
|
||||
}
|
||||
@@ -1561,7 +1561,7 @@ func TestOptType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "CALL", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewOptType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewOptType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new OptType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1589,7 +1589,7 @@ func TestOptType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStockType(t *testing.T) {
|
||||
e, err := ofxgo.NewStockType("COMMON")
|
||||
e, err := NewStockType("COMMON")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new StockType from string \"COMMON\"\n")
|
||||
}
|
||||
@@ -1606,7 +1606,7 @@ func TestStockType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewStockType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewStockType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new StockType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1634,7 +1634,7 @@ func TestStockType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHolderType(t *testing.T) {
|
||||
e, err := ofxgo.NewHolderType("INDIVIDUAL")
|
||||
e, err := NewHolderType("INDIVIDUAL")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new HolderType from string \"INDIVIDUAL\"\n")
|
||||
}
|
||||
@@ -1651,7 +1651,7 @@ func TestHolderType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewHolderType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewHolderType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new HolderType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1679,7 +1679,7 @@ func TestHolderType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAcctClassification(t *testing.T) {
|
||||
e, err := ofxgo.NewAcctClassification("PERSONAL")
|
||||
e, err := NewAcctClassification("PERSONAL")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new AcctClassification from string \"PERSONAL\"\n")
|
||||
}
|
||||
@@ -1696,7 +1696,7 @@ func TestAcctClassification(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "OTHER", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewAcctClassification("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewAcctClassification("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new AcctClassification from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1724,7 +1724,7 @@ func TestAcctClassification(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSvcStatus(t *testing.T) {
|
||||
e, err := ofxgo.NewSvcStatus("AVAIL")
|
||||
e, err := NewSvcStatus("AVAIL")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new SvcStatus from string \"AVAIL\"\n")
|
||||
}
|
||||
@@ -1741,7 +1741,7 @@ func TestSvcStatus(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "ACTIVE", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewSvcStatus("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewSvcStatus("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new SvcStatus from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
@@ -1769,7 +1769,7 @@ func TestSvcStatus(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUsProductType(t *testing.T) {
|
||||
e, err := ofxgo.NewUsProductType("401K")
|
||||
e, err := NewUsProductType("401K")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating new UsProductType from string \"401K\"\n")
|
||||
}
|
||||
@@ -1786,7 +1786,7 @@ func TestUsProductType(t *testing.T) {
|
||||
|
||||
marshalHelper(t, "UGMA", &e)
|
||||
|
||||
overwritten, err := ofxgo.NewUsProductType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := NewUsProductType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error creating new UsProductType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -41,31 +40,31 @@ func TestMarshalCCStatementRequest(t *testing.T) {
|
||||
</CREDITCARDMSGSRQV1>
|
||||
</OFX>`
|
||||
|
||||
var client = ofxgo.Client{
|
||||
var client = BasicClient{
|
||||
AppID: "OFXGO",
|
||||
AppVer: "0001",
|
||||
SpecVersion: ofxgo.OfxVersion203,
|
||||
SpecVersion: OfxVersion203,
|
||||
}
|
||||
|
||||
var request ofxgo.Request
|
||||
var request Request
|
||||
request.Signon.UserID = "myusername"
|
||||
request.Signon.UserPass = "Pa$$word"
|
||||
request.Signon.Org = "BNK"
|
||||
request.Signon.Fid = "1987"
|
||||
|
||||
statementRequest := ofxgo.CCStatementRequest{
|
||||
statementRequest := CCStatementRequest{
|
||||
TrnUID: "913846",
|
||||
CCAcctFrom: ofxgo.CCAcct{
|
||||
CCAcctFrom: CCAcct{
|
||||
AcctID: "XXXXXXXXXXXX1234",
|
||||
},
|
||||
DtStart: ofxgo.NewDateGMT(2017, 1, 1, 0, 0, 0, 0),
|
||||
DtStart: NewDateGMT(2017, 1, 1, 0, 0, 0, 0),
|
||||
Include: true,
|
||||
}
|
||||
request.CreditCard = append(request.CreditCard, &statementRequest)
|
||||
|
||||
request.SetClientFields(&client)
|
||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||
request.Signon.DtClient = *ofxgo.NewDateGMT(2017, 3, 31, 15, 38, 48, 0)
|
||||
request.Signon.DtClient = *NewDateGMT(2017, 3, 31, 15, 38, 48, 0)
|
||||
|
||||
marshalCheckRequest(t, &request, expectedString)
|
||||
}
|
||||
@@ -82,45 +81,45 @@ OLDFILEUID:NONE
|
||||
NEWFILEUID:NONE
|
||||
|
||||
<OFX><SIGNONMSGSRSV1><SONRS><STATUS><CODE>0<SEVERITY>INFO<MESSAGE>SUCCESS</STATUS><DTSERVER>20170331154648.331[-4:EDT]<LANGUAGE>ENG<FI><ORG>01<FID>81729</FI></SONRS></SIGNONMSGSRSV1><CREDITCARDMSGSRSV1><CCSTMTTRNRS><TRNUID>59e850ad-7448-b4ce-4b71-29057763b306<STATUS><CODE>0<SEVERITY>INFO</STATUS><CCSTMTRS><CURDEF>USD<CCACCTFROM><ACCTID>9283744488463775</CCACCTFROM><BANKTRANLIST><DTSTART>20161201154648.688[-5:EST]<DTEND>20170331154648.688[-4:EDT]<STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170209120000[0:GMT]<TRNAMT>-7.96<FITID>2017020924435657040207171600195<NAME>SLICE OF NY</STMTTRN><STMTTRN><TRNTYPE>CREDIT<DTPOSTED>20161228120000[0:GMT]<TRNAMT>3830.46<FITID>2016122823633637200000258482730<NAME>Payment Thank You Electro</STMTTRN><STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170327120000[0:GMT]<TRNAMT>-17.7<FITID>2017032724445727085300442885680<NAME>KROGER FUEL #9999</STMTTRN></BANKTRANLIST><LEDGERBAL><BALAMT>-9334<DTASOF>20170331080000.000[-4:EDT]</LEDGERBAL><AVAILBAL><BALAMT>7630.17<DTASOF>20170331080000.000[-4:EDT]</AVAILBAL></CCSTMTRS></CCSTMTTRNRS></CREDITCARDMSGSRSV1></OFX>`)
|
||||
var expected ofxgo.Response
|
||||
var expected Response
|
||||
EDT := time.FixedZone("EDT", -4*60*60)
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
|
||||
expected.Version = ofxgo.OfxVersion102
|
||||
expected.Version = OfxVersion102
|
||||
expected.Signon.Status.Code = 0
|
||||
expected.Signon.Status.Severity = "INFO"
|
||||
expected.Signon.Status.Message = "SUCCESS"
|
||||
expected.Signon.DtServer = *ofxgo.NewDate(2017, 3, 31, 15, 46, 48, 331000000, EDT)
|
||||
expected.Signon.DtServer = *NewDate(2017, 3, 31, 15, 46, 48, 331000000, EDT)
|
||||
expected.Signon.Language = "ENG"
|
||||
expected.Signon.Org = "01"
|
||||
expected.Signon.Fid = "81729"
|
||||
|
||||
var trnamt1, trnamt2, trnamt3 ofxgo.Amount
|
||||
var trnamt1, trnamt2, trnamt3 Amount
|
||||
trnamt1.SetFrac64(-796, 100)
|
||||
trnamt2.SetFrac64(383046, 100)
|
||||
trnamt3.SetFrac64(-1770, 100)
|
||||
|
||||
banktranlist := ofxgo.TransactionList{
|
||||
DtStart: *ofxgo.NewDate(2016, 12, 1, 15, 46, 48, 688000000, EST),
|
||||
DtEnd: *ofxgo.NewDate(2017, 3, 31, 15, 46, 48, 688000000, EDT),
|
||||
Transactions: []ofxgo.Transaction{
|
||||
banktranlist := TransactionList{
|
||||
DtStart: *NewDate(2016, 12, 1, 15, 46, 48, 688000000, EST),
|
||||
DtEnd: *NewDate(2017, 3, 31, 15, 46, 48, 688000000, EDT),
|
||||
Transactions: []Transaction{
|
||||
{
|
||||
TrnType: ofxgo.TrnTypeDebit,
|
||||
DtPosted: *ofxgo.NewDateGMT(2017, 2, 9, 12, 0, 0, 0),
|
||||
TrnType: TrnTypeDebit,
|
||||
DtPosted: *NewDateGMT(2017, 2, 9, 12, 0, 0, 0),
|
||||
TrnAmt: trnamt1,
|
||||
FiTID: "2017020924435657040207171600195",
|
||||
Name: "SLICE OF NY",
|
||||
},
|
||||
{
|
||||
TrnType: ofxgo.TrnTypeCredit,
|
||||
DtPosted: *ofxgo.NewDateGMT(2016, 12, 28, 12, 0, 0, 0),
|
||||
TrnType: TrnTypeCredit,
|
||||
DtPosted: *NewDateGMT(2016, 12, 28, 12, 0, 0, 0),
|
||||
TrnAmt: trnamt2,
|
||||
FiTID: "2016122823633637200000258482730",
|
||||
Name: "Payment Thank You Electro",
|
||||
},
|
||||
{
|
||||
TrnType: ofxgo.TrnTypeDebit,
|
||||
DtPosted: *ofxgo.NewDateGMT(2017, 3, 27, 12, 0, 0, 0),
|
||||
TrnType: TrnTypeDebit,
|
||||
DtPosted: *NewDateGMT(2017, 3, 27, 12, 0, 0, 0),
|
||||
TrnAmt: trnamt3,
|
||||
FiTID: "2017032724445727085300442885680",
|
||||
Name: "KROGER FUEL #9999",
|
||||
@@ -128,37 +127,38 @@ NEWFILEUID:NONE
|
||||
},
|
||||
}
|
||||
|
||||
var balamt, availbalamt ofxgo.Amount
|
||||
var balamt, availbalamt Amount
|
||||
balamt.SetFrac64(-933400, 100)
|
||||
availbalamt.SetFrac64(763017, 100)
|
||||
|
||||
usd, err := ofxgo.NewCurrSymbol("USD")
|
||||
usd, err := NewCurrSymbol("USD")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating CurrSymbol for USD\n")
|
||||
}
|
||||
|
||||
statementResponse := ofxgo.CCStatementResponse{
|
||||
statementResponse := CCStatementResponse{
|
||||
TrnUID: "59e850ad-7448-b4ce-4b71-29057763b306",
|
||||
Status: ofxgo.Status{
|
||||
Status: Status{
|
||||
Code: 0,
|
||||
Severity: "INFO",
|
||||
},
|
||||
CurDef: *usd,
|
||||
CCAcctFrom: ofxgo.CCAcct{
|
||||
CCAcctFrom: CCAcct{
|
||||
AcctID: "9283744488463775",
|
||||
},
|
||||
BankTranList: &banktranlist,
|
||||
BalAmt: balamt,
|
||||
DtAsOf: *ofxgo.NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
||||
DtAsOf: *NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
||||
AvailBalAmt: &availbalamt,
|
||||
AvailDtAsOf: ofxgo.NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
||||
AvailDtAsOf: NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
||||
}
|
||||
expected.CreditCard = append(expected.CreditCard, &statementResponse)
|
||||
|
||||
response, err := ofxgo.ParseResponse(responseReader)
|
||||
response, err := ParseResponse(responseReader)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||
}
|
||||
|
||||
checkResponsesEqual(t, &expected, response)
|
||||
checkResponseRoundTrip(t, response)
|
||||
}
|
||||
|
||||
103
discovercard_client.go
Normal file
103
discovercard_client.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DiscoverCardClient provides a Client implementation which handles
|
||||
// DiscoverCard's broken HTTP header behavior. DiscoverCardClient uses default,
|
||||
// non-zero settings, if its fields are not initialized.
|
||||
type DiscoverCardClient struct {
|
||||
*BasicClient
|
||||
}
|
||||
|
||||
// NewDiscoverCardClient returns a Client interface configured to handle
|
||||
// Discover Card's brand of idiosyncracy
|
||||
func NewDiscoverCardClient(bc *BasicClient) Client {
|
||||
return &DiscoverCardClient{bc}
|
||||
}
|
||||
|
||||
func discoverCardHTTPPost(URL string, r io.Reader) (*http.Response, error) {
|
||||
// Either convert or copy to a bytes.Buffer to be able to determine the
|
||||
// request length for the Content-Length header
|
||||
buf, ok := r.(*bytes.Buffer)
|
||||
if !ok {
|
||||
buf = &bytes.Buffer{}
|
||||
_, err := io.Copy(buf, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
url, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := url.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
// Discover requires only these headers and in this exact order, or it
|
||||
// returns HTTP 403
|
||||
headers := fmt.Sprintf("POST %s HTTP/1.1\r\n"+
|
||||
"Content-Type: application/x-ofx\r\n"+
|
||||
"Host: %s\r\n"+
|
||||
"Content-Length: %d\r\n"+
|
||||
"Connection: Keep-Alive\r\n"+
|
||||
"\r\n", path, url.Hostname(), buf.Len())
|
||||
|
||||
host := url.Host
|
||||
if url.Port() == "" {
|
||||
host += ":443"
|
||||
}
|
||||
|
||||
// BUGBUG: cannot do defer conn.Close() until body is read,
|
||||
// we are "leaking" a socket here, but it will be finalized
|
||||
conn, err := tls.Dial("tcp", host, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Fprint(conn, headers)
|
||||
_, err = io.Copy(conn, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.ReadResponse(bufio.NewReader(conn), nil)
|
||||
}
|
||||
|
||||
func (c *DiscoverCardClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
||||
if !strings.HasPrefix(URL, "https://") {
|
||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||
}
|
||||
|
||||
response, err := discoverCardHTTPPost(URL, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *DiscoverCardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||
return clientRequestNoParse(c, r)
|
||||
}
|
||||
|
||||
func (c *DiscoverCardClient) Request(r *Request) (*Response, error) {
|
||||
return clientRequest(c, r)
|
||||
}
|
||||
27
doc.go
27
doc.go
@@ -71,33 +71,32 @@ account and print the balance:
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"os"
|
||||
)
|
||||
|
||||
var client ofxgo.Client // By not initializing them, we accept all default
|
||||
var client Client // By not initializing them, we accept all default
|
||||
// client values
|
||||
var request ofxgo.Request
|
||||
var request Request
|
||||
|
||||
// These are all specific to you and your financial institution
|
||||
request.URL = "https://ofx.example.com"
|
||||
request.Signon.UserID = ofxgo.String("john")
|
||||
request.Signon.UserPass = ofxgo.String("hunter2")
|
||||
request.Signon.Org = ofxgo.String("MyBank")
|
||||
request.Signon.Fid = ofxgo.String("0001")
|
||||
request.Signon.UserID = String("john")
|
||||
request.Signon.UserPass = String("hunter2")
|
||||
request.Signon.Org = String("MyBank")
|
||||
request.Signon.Fid = String("0001")
|
||||
|
||||
uid, err := ofxgo.RandomUID()
|
||||
uid, err := RandomUID()
|
||||
if err != nil {
|
||||
fmt.Println("Error creating uid for transaction:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
statementRequest := ofxgo.StatementRequest{
|
||||
statementRequest := StatementRequest{
|
||||
TrnUID: *uid,
|
||||
BankAcctFrom: ofxgo.BankAcct{
|
||||
BankID: ofxgo.String("123456789"),
|
||||
AcctID: ofxgo.String("11111111111"),
|
||||
AcctType: ofxgo.AcctTypeChecking,
|
||||
BankAcctFrom: BankAcct{
|
||||
BankID: String("123456789"),
|
||||
AcctID: String("11111111111"),
|
||||
AcctType: AcctTypeChecking,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -117,7 +116,7 @@ account and print the balance:
|
||||
|
||||
if len(response.Bank) < 1 {
|
||||
fmt.Println("No banking messages received")
|
||||
} else if stmt, ok := response.Bank[0].(*ofxgo.StatementResponse); ok {
|
||||
} else if stmt, ok := response.Bank[0].(*StatementResponse); ok {
|
||||
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@ header = """package ofxgo
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/aclindsa/xml"
|
||||
"strings"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -164,7 +165,7 @@ with open("constants.go", 'w') as f:
|
||||
constNames=constNames,
|
||||
upperValueString=upperValueString))
|
||||
|
||||
test_header = """package ofxgo_test
|
||||
test_header = """package ofxgo
|
||||
|
||||
/*
|
||||
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
|
||||
@@ -173,16 +174,16 @@ test_header = """package ofxgo_test
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/xml"
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
"""
|
||||
|
||||
test_template = """
|
||||
func Test{enum}(t *testing.T) {{
|
||||
e, err := ofxgo.New{enum}("{firstValueUpper}")
|
||||
e, err := New{enum}("{firstValueUpper}")
|
||||
if err != nil {{
|
||||
t.Fatalf("Unexpected error creating new {enum} from string \\\"{firstValueUpper}\\\"\\n")
|
||||
}}
|
||||
@@ -199,7 +200,7 @@ func Test{enum}(t *testing.T) {{
|
||||
|
||||
marshalHelper(t, "{lastValueUpper}", &e)
|
||||
|
||||
overwritten, err := ofxgo.New{enum}("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
overwritten, err := New{enum}("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||
if err == nil {{
|
||||
t.Fatalf("Expected error creating new {enum} from string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\"\\n")
|
||||
}}
|
||||
|
||||
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module github.com/aclindsa/ofxgo
|
||||
|
||||
require (
|
||||
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2
|
||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
|
||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 // indirect
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect
|
||||
golang.org/x/text v0.0.0-20180911161511-905a57155faa
|
||||
)
|
||||
|
||||
go 1.9
|
||||
14
go.sum
Normal file
14
go.sum
Normal file
@@ -0,0 +1,14 @@
|
||||
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20 h1:wN3KlzWq56AIgOqFzYLYVih4zVyPDViCUeG5uZxJHq4=
|
||||
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20/go.mod h1:DiEHtTD+e6zS3+R95F05Bfbcsfv13wZTi2M4LfAFLBE=
|
||||
github.com/aclindsa/xml v0.0.0-20190625094425-0aa7a3409cf4 h1:STo5wlCItpgL9LFBui17kZ/N1iKQk+UztLRj2cVkSXQ=
|
||||
github.com/aclindsa/xml v0.0.0-20190625094425-0aa7a3409cf4/go.mod h1:GjqOUT8xlg5+T19lFv6yAGNrtMKkZ839Gt4e16mBXlY=
|
||||
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2 h1:ICeGSGrc6fd81VtQ3nZ2h7GEOKxWYwRxjW0v0d/mgu4=
|
||||
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2/go.mod h1:GjqOUT8xlg5+T19lFv6yAGNrtMKkZ839Gt4e16mBXlY=
|
||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
|
||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc=
|
||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
|
||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.0.0-20180911161511-905a57155faa h1:uIJ7KxPgS7ODNO//HqlPfjWmWDGRsoONAVcEVaJNWNs=
|
||||
golang.org/x/text v0.0.0-20180911161511-905a57155faa/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
216
invstmt.go
216
invstmt.go
@@ -465,6 +465,7 @@ type InvBankTransaction struct {
|
||||
// security-related transactions themselves. It must be unmarshalled manually
|
||||
// due to the structure (don't know what kind of InvTransaction is coming next)
|
||||
type InvTranList struct {
|
||||
XMLName xml.Name `xml:"INVTRANLIST"`
|
||||
DtStart Date
|
||||
DtEnd Date // This is the value that should be sent as <DTSTART> in the next InvStatementRequest to ensure that no transactions are missed
|
||||
InvTransactions []InvTransaction
|
||||
@@ -630,6 +631,119 @@ func (l *InvTranList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalXML handles marshalling an InvTranList element to an SGML/XML string
|
||||
func (l *InvTranList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
invTranListElement := xml.StartElement{Name: xml.Name{Local: "INVTRANLIST"}}
|
||||
if err := e.EncodeToken(invTranListElement); err != nil {
|
||||
return err
|
||||
}
|
||||
err := e.EncodeElement(&l.DtStart, xml.StartElement{Name: xml.Name{Local: "DTSTART"}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.EncodeElement(&l.DtEnd, xml.StartElement{Name: xml.Name{Local: "DTEND"}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range l.InvTransactions {
|
||||
start := xml.StartElement{Name: xml.Name{Local: t.TransactionType()}}
|
||||
switch tran := t.(type) {
|
||||
case BuyDebt:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case BuyMF:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case BuyOpt:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case BuyOther:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case BuyStock:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case ClosureOpt:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case Income:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case InvExpense:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case JrnlFund:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case JrnlSec:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case MarginInterest:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case Reinvest:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case RetOfCap:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case SellDebt:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case SellMF:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case SellOpt:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case SellOther:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case SellStock:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case Split:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case Transfer:
|
||||
if err := e.EncodeElement(&tran, start); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("Invalid INVTRANLIST child type: " + tran.TransactionType())
|
||||
}
|
||||
}
|
||||
for _, tran := range l.BankTransactions {
|
||||
err = e.EncodeElement(&tran, xml.StartElement{Name: xml.Name{Local: "INVBANKTRAN"}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := e.EncodeToken(invTranListElement.End()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvPosition contains generic position information included in each of the
|
||||
// other *Position types
|
||||
type InvPosition struct {
|
||||
@@ -770,6 +884,45 @@ func (p *PositionList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalXML handles marshalling a PositionList to an XML string
|
||||
func (p *PositionList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
invPosListElement := xml.StartElement{Name: xml.Name{Local: "INVPOSLIST"}}
|
||||
if err := e.EncodeToken(invPosListElement); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, position := range *p {
|
||||
start := xml.StartElement{Name: xml.Name{Local: position.PositionType()}}
|
||||
switch pos := position.(type) {
|
||||
case DebtPosition:
|
||||
if err := e.EncodeElement(&pos, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case MFPosition:
|
||||
if err := e.EncodeElement(&pos, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OptPosition:
|
||||
if err := e.EncodeElement(&pos, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OtherPosition:
|
||||
if err := e.EncodeElement(&pos, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case StockPosition:
|
||||
if err := e.EncodeElement(&pos, start); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("Invalid INVPOSLIST child type: " + pos.PositionType())
|
||||
}
|
||||
}
|
||||
if err := e.EncodeToken(invPosListElement.End()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvBalance contains three (or optionally four) specified balances as well as
|
||||
// a free-form list of generic balance information which may be provided by an
|
||||
// FI.
|
||||
@@ -1036,6 +1189,69 @@ func (o *OOList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalXML handles marshalling an OOList to an XML string
|
||||
func (o *OOList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
ooListElement := xml.StartElement{Name: xml.Name{Local: "INVOOLIST"}}
|
||||
if err := e.EncodeToken(ooListElement); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, openorder := range *o {
|
||||
start := xml.StartElement{Name: xml.Name{Local: openorder.OrderType()}}
|
||||
switch oo := openorder.(type) {
|
||||
case OOBuyDebt:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOBuyMF:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOBuyOpt:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOBuyOther:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOBuyStock:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOSellDebt:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOSellMF:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOSellOpt:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOSellOther:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOSellStock:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OOSwitchMF:
|
||||
if err := e.EncodeElement(&oo, start); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("Invalid OOLIST child type: " + oo.OrderType())
|
||||
}
|
||||
}
|
||||
if err := e.EncodeToken(ooListElement.End()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContribSecurity identifies current contribution allocation for a security in
|
||||
// a 401(k) account
|
||||
type ContribSecurity struct {
|
||||
|
||||
743
invstmt_test.go
743
invstmt_test.go
File diff suppressed because it is too large
Load Diff
@@ -172,6 +172,7 @@ var ofxLeafElements = []string{
|
||||
"IDSCOPE",
|
||||
"INCBAL",
|
||||
"INCIMAGES",
|
||||
"INCLUDE",
|
||||
"INCOMETYPE",
|
||||
"INCOO",
|
||||
"INITIALAMT",
|
||||
|
||||
30
profile.go
30
profile.go
@@ -3,6 +3,7 @@ package ofxgo
|
||||
import (
|
||||
"errors"
|
||||
"github.com/aclindsa/xml"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProfileRequest represents a request for a server to provide a profile of its
|
||||
@@ -126,6 +127,35 @@ func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalXML handles marshalling a MessageSetList element to an XML string
|
||||
func (msl *MessageSetList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
messageSetListElement := xml.StartElement{Name: xml.Name{Local: "MSGSETLIST"}}
|
||||
if err := e.EncodeToken(messageSetListElement); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, messageset := range *msl {
|
||||
if !strings.HasSuffix(messageset.Name, "V1") {
|
||||
return errors.New("Expected MessageSet.Name to end with \"V1\"")
|
||||
}
|
||||
messageSetName := strings.TrimSuffix(messageset.Name, "V1")
|
||||
messageSetElement := xml.StartElement{Name: xml.Name{Local: messageSetName}}
|
||||
if err := e.EncodeToken(messageSetElement); err != nil {
|
||||
return err
|
||||
}
|
||||
start := xml.StartElement{Name: xml.Name{Local: messageset.Name}}
|
||||
if err := e.EncodeElement(&messageset, start); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.EncodeToken(messageSetElement.End()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := e.EncodeToken(messageSetListElement.End()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProfileResponse contains a requested profile of the server's capabilities
|
||||
// (which message sets and versions it supports, how to access them, which
|
||||
// languages and which types of synchronization they support, etc.). Note that
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -36,13 +35,13 @@ func TestMarshalProfileRequest(t *testing.T) {
|
||||
</PROFMSGSRQV1>
|
||||
</OFX>`
|
||||
|
||||
var client = ofxgo.Client{
|
||||
var client = BasicClient{
|
||||
AppID: "OFXGO",
|
||||
AppVer: "0001",
|
||||
SpecVersion: ofxgo.OfxVersion203,
|
||||
SpecVersion: OfxVersion203,
|
||||
}
|
||||
|
||||
var request ofxgo.Request
|
||||
var request Request
|
||||
request.Signon.UserID = "anonymous00000000000000000000000"
|
||||
request.Signon.UserPass = "anonymous00000000000000000000000"
|
||||
request.Signon.Org = "BNK"
|
||||
@@ -50,15 +49,15 @@ func TestMarshalProfileRequest(t *testing.T) {
|
||||
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
|
||||
profileRequest := ofxgo.ProfileRequest{
|
||||
profileRequest := ProfileRequest{
|
||||
TrnUID: "983373",
|
||||
DtProfUp: *ofxgo.NewDate(2016, 1, 1, 0, 0, 0, 0, EST),
|
||||
DtProfUp: *NewDate(2016, 1, 1, 0, 0, 0, 0, EST),
|
||||
}
|
||||
request.Prof = append(request.Prof, &profileRequest)
|
||||
|
||||
request.SetClientFields(&client)
|
||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||
request.Signon.DtClient = *ofxgo.NewDate(2016, 6, 14, 7, 34, 0, 0, EST)
|
||||
request.Signon.DtClient = *NewDate(2016, 6, 14, 7, 34, 0, 0, EST)
|
||||
|
||||
marshalCheckRequest(t, &request, expectedString)
|
||||
}
|
||||
@@ -213,89 +212,89 @@ NEWFILEUID:NONE
|
||||
</PROFTRNRS>
|
||||
</PROFMSGSRSV1>
|
||||
</OFX>`)
|
||||
var expected ofxgo.Response
|
||||
var expected Response
|
||||
|
||||
expected.Version = ofxgo.OfxVersion102
|
||||
expected.Version = OfxVersion102
|
||||
expected.Signon.Status.Code = 0
|
||||
expected.Signon.Status.Severity = "INFO"
|
||||
expected.Signon.DtServer = *ofxgo.NewDateGMT(2017, 4, 3, 9, 34, 58, 0)
|
||||
expected.Signon.DtServer = *NewDateGMT(2017, 4, 3, 9, 34, 58, 0)
|
||||
expected.Signon.Language = "ENG"
|
||||
expected.Signon.DtProfUp = ofxgo.NewDateGMT(2002, 11, 19, 14, 0, 0, 0)
|
||||
expected.Signon.DtProfUp = NewDateGMT(2002, 11, 19, 14, 0, 0, 0)
|
||||
|
||||
profileResponse := ofxgo.ProfileResponse{
|
||||
profileResponse := ProfileResponse{
|
||||
TrnUID: "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab",
|
||||
Status: ofxgo.Status{
|
||||
Status: Status{
|
||||
Code: 0,
|
||||
Severity: "INFO",
|
||||
},
|
||||
MessageSetList: ofxgo.MessageSetList{
|
||||
ofxgo.MessageSet{
|
||||
MessageSetList: MessageSetList{
|
||||
MessageSet{
|
||||
Name: "SIGNONMSGSETV1",
|
||||
Ver: 1,
|
||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||
OfxSec: ofxgo.OfxSecNone,
|
||||
OfxSec: OfxSecNone,
|
||||
TranspSec: true,
|
||||
SignonRealm: "Example Trade",
|
||||
Language: []ofxgo.String{"ENG"},
|
||||
SyncMode: ofxgo.SyncModeLite,
|
||||
Language: []String{"ENG"},
|
||||
SyncMode: SyncModeLite,
|
||||
RespFileER: false,
|
||||
// Ignored: <INTU.TIMEOUT>300
|
||||
},
|
||||
ofxgo.MessageSet{
|
||||
MessageSet{
|
||||
Name: "SIGNUPMSGSETV1",
|
||||
Ver: 1,
|
||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||
OfxSec: ofxgo.OfxSecNone,
|
||||
OfxSec: OfxSecNone,
|
||||
TranspSec: true,
|
||||
SignonRealm: "Example Trade",
|
||||
Language: []ofxgo.String{"ENG"},
|
||||
SyncMode: ofxgo.SyncModeLite,
|
||||
Language: []String{"ENG"},
|
||||
SyncMode: SyncModeLite,
|
||||
RespFileER: false,
|
||||
// Ignored: <INTU.TIMEOUT>300
|
||||
},
|
||||
ofxgo.MessageSet{
|
||||
MessageSet{
|
||||
Name: "INVSTMTMSGSETV1",
|
||||
Ver: 1,
|
||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||
OfxSec: ofxgo.OfxSecNone,
|
||||
OfxSec: OfxSecNone,
|
||||
TranspSec: true,
|
||||
SignonRealm: "Example Trade",
|
||||
Language: []ofxgo.String{"ENG"},
|
||||
SyncMode: ofxgo.SyncModeLite,
|
||||
Language: []String{"ENG"},
|
||||
SyncMode: SyncModeLite,
|
||||
RespFileER: false,
|
||||
// Ignored: <INTU.TIMEOUT>300
|
||||
},
|
||||
ofxgo.MessageSet{
|
||||
MessageSet{
|
||||
Name: "SECLISTMSGSETV1",
|
||||
Ver: 1,
|
||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||
OfxSec: ofxgo.OfxSecNone,
|
||||
OfxSec: OfxSecNone,
|
||||
TranspSec: true,
|
||||
SignonRealm: "Example Trade",
|
||||
Language: []ofxgo.String{"ENG"},
|
||||
SyncMode: ofxgo.SyncModeLite,
|
||||
Language: []String{"ENG"},
|
||||
SyncMode: SyncModeLite,
|
||||
RespFileER: false,
|
||||
// Ignored: <INTU.TIMEOUT>300
|
||||
},
|
||||
ofxgo.MessageSet{
|
||||
MessageSet{
|
||||
Name: "PROFMSGSETV1",
|
||||
Ver: 1,
|
||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||
OfxSec: ofxgo.OfxSecNone,
|
||||
OfxSec: OfxSecNone,
|
||||
TranspSec: true,
|
||||
SignonRealm: "Example Trade",
|
||||
Language: []ofxgo.String{"ENG"},
|
||||
SyncMode: ofxgo.SyncModeLite,
|
||||
Language: []String{"ENG"},
|
||||
SyncMode: SyncModeLite,
|
||||
RespFileER: false,
|
||||
// Ignored: <INTU.TIMEOUT>300
|
||||
},
|
||||
},
|
||||
SignonInfoList: []ofxgo.SignonInfo{
|
||||
SignonInfoList: []SignonInfo{
|
||||
{
|
||||
SignonRealm: "Example Trade",
|
||||
Min: 1,
|
||||
Max: 32,
|
||||
CharType: ofxgo.CharTypeAlphaOrNumeric,
|
||||
CharType: CharTypeAlphaOrNumeric,
|
||||
CaseSen: false,
|
||||
Special: true,
|
||||
Spaces: false,
|
||||
@@ -303,7 +302,7 @@ NEWFILEUID:NONE
|
||||
ChgPinFirst: false,
|
||||
},
|
||||
},
|
||||
DtProfUp: *ofxgo.NewDateGMT(2002, 11, 19, 14, 0, 0, 0),
|
||||
DtProfUp: *NewDateGMT(2002, 11, 19, 14, 0, 0, 0),
|
||||
FiName: "Example Trade Financial",
|
||||
Addr1: "5555 Buhunkus Drive",
|
||||
City: "Someville",
|
||||
@@ -319,10 +318,11 @@ NEWFILEUID:NONE
|
||||
}
|
||||
expected.Prof = append(expected.Prof, &profileResponse)
|
||||
|
||||
response, err := ofxgo.ParseResponse(responseReader)
|
||||
response, err := ParseResponse(responseReader)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||
}
|
||||
|
||||
checkResponsesEqual(t, &expected, response)
|
||||
checkResponseRoundTrip(t, response)
|
||||
}
|
||||
|
||||
38
request.go
38
request.go
@@ -3,7 +3,6 @@ package ofxgo
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/aclindsa/xml"
|
||||
"time"
|
||||
)
|
||||
@@ -32,10 +31,11 @@ type Request struct {
|
||||
Prof []Message //<PROFMSGSETV1>
|
||||
Image []Message //<IMAGEMSGSETV1>
|
||||
|
||||
indent bool // Whether to indent the marshaled XML
|
||||
indent bool // Whether to indent the marshaled XML
|
||||
carriageReturn bool // Whether to user carriage returns in new lines for marshaled XML
|
||||
}
|
||||
|
||||
func marshalMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error {
|
||||
func encodeMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error {
|
||||
if len(requests) > 0 {
|
||||
messageSetElement := xml.StartElement{Name: xml.Name{Local: set.String()}}
|
||||
if err := e.EncodeToken(messageSetElement); err != nil {
|
||||
@@ -63,7 +63,7 @@ func marshalMessageSet(e *xml.Encoder, requests []Message, set messageType, vers
|
||||
|
||||
// SetClientFields overwrites the fields in this Request object controlled by
|
||||
// the Client
|
||||
func (oq *Request) SetClientFields(c *Client) {
|
||||
func (oq *Request) SetClientFields(c Client) {
|
||||
oq.Signon.DtClient.Time = time.Now()
|
||||
|
||||
// Overwrite fields that the client controls
|
||||
@@ -71,6 +71,7 @@ func (oq *Request) SetClientFields(c *Client) {
|
||||
oq.Signon.AppID = c.ID()
|
||||
oq.Signon.AppVer = c.Version()
|
||||
oq.indent = c.IndentRequests()
|
||||
oq.carriageReturn = c.CarriageReturnNewLines()
|
||||
}
|
||||
|
||||
// Marshal this Request into its SGML/XML representation held in a bytes.Buffer
|
||||
@@ -80,30 +81,19 @@ func (oq *Request) Marshal() (*bytes.Buffer, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
// Write the header appropriate to our version
|
||||
switch oq.Version {
|
||||
case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160:
|
||||
b.WriteString(`OFXHEADER:100
|
||||
DATA:OFXSGML
|
||||
VERSION:` + oq.Version.String() + `
|
||||
SECURITY:NONE
|
||||
ENCODING:USASCII
|
||||
CHARSET:1252
|
||||
COMPRESSION:NONE
|
||||
OLDFILEUID:NONE
|
||||
NEWFILEUID:NONE
|
||||
|
||||
`)
|
||||
case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220:
|
||||
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + "\n")
|
||||
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + oq.Version.String() + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>` + "\n")
|
||||
default:
|
||||
return nil, fmt.Errorf("%d is not a valid OFX version string", oq.Version)
|
||||
}
|
||||
writeHeader(&b, oq.Version, oq.carriageReturn)
|
||||
|
||||
encoder := xml.NewEncoder(&b)
|
||||
if oq.indent {
|
||||
encoder.Indent("", " ")
|
||||
}
|
||||
if oq.carriageReturn {
|
||||
encoder.CarriageReturn(true)
|
||||
}
|
||||
if oq.Version < OfxVersion200 {
|
||||
// OFX 100 series versions should avoid element close tags for compatibility
|
||||
encoder.SetDisableAutoClose(ofxLeafElements...)
|
||||
}
|
||||
|
||||
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
||||
|
||||
@@ -145,7 +135,7 @@ NEWFILEUID:NONE
|
||||
{oq.Image, ImageRq},
|
||||
}
|
||||
for _, set := range messageSets {
|
||||
if err := marshalMessageSet(encoder, set.Messages, set.Type, oq.Version); err != nil {
|
||||
if err := encodeMessageSet(encoder, set.Messages, set.Type, oq.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var ignoreSpacesRe = regexp.MustCompile(">[ \t\r\n]+<")
|
||||
// match leading and trailing whitespace on each line
|
||||
var ignoreSpacesRe = regexp.MustCompile("(?m)^[ \t]+|[ \t]*$[\r\n]+")
|
||||
|
||||
func marshalCheckRequest(t *testing.T, request *ofxgo.Request, expected string) {
|
||||
func marshalCheckRequest(t *testing.T, request *Request, expected string) {
|
||||
t.Helper()
|
||||
buf, err := request.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unexpected error marshalling request: %s\n", t.Name(), err)
|
||||
@@ -17,8 +18,8 @@ func marshalCheckRequest(t *testing.T, request *ofxgo.Request, expected string)
|
||||
actualString := buf.String()
|
||||
|
||||
// Ignore spaces between XML elements
|
||||
expectedString := ignoreSpacesRe.ReplaceAllString(expected, "><")
|
||||
actualString = ignoreSpacesRe.ReplaceAllString(actualString, "><")
|
||||
expectedString := ignoreSpacesRe.ReplaceAllString(expected, "")
|
||||
actualString = ignoreSpacesRe.ReplaceAllString(actualString, "")
|
||||
|
||||
if expectedString != actualString {
|
||||
compareLength := len(expectedString)
|
||||
|
||||
164
response.go
164
response.go
@@ -4,10 +4,12 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/aclindsa/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
|
||||
// Response is the top-level object returned from a parsed OFX response file.
|
||||
@@ -35,6 +37,16 @@ type Response struct {
|
||||
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
|
||||
var seenHeader, seenVersion bool = false, false
|
||||
for {
|
||||
// Some financial institutions do not properly leave an empty line after the last header.
|
||||
// Avoid attempting to read another header in that case.
|
||||
next, err := r.Peek(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if next[0] == '<' {
|
||||
break
|
||||
}
|
||||
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -175,6 +187,7 @@ const guessVersionCheckBytes = 1024
|
||||
|
||||
// Defaults to XML if it can't determine the version or if there is any
|
||||
// ambiguity
|
||||
// Returns false for SGML, true (for XML) otherwise.
|
||||
func guessVersion(r *bufio.Reader) (bool, error) {
|
||||
b, _ := r.Peek(guessVersionCheckBytes)
|
||||
if b == nil {
|
||||
@@ -245,9 +258,6 @@ func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, v
|
||||
if err := d.DecodeElement(responseMessage, &startElement); err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, err := responseMessage.Valid(version); !ok {
|
||||
return err
|
||||
}
|
||||
*msgs = append(*msgs, responseMessage)
|
||||
} else {
|
||||
return errors.New("Didn't find an opening element")
|
||||
@@ -255,14 +265,25 @@ func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, v
|
||||
}
|
||||
}
|
||||
|
||||
// ParseResponse parses an OFX response in SGML or XML into a Response object
|
||||
// from the given io.Reader
|
||||
// ParseResponse parses and validates an OFX response in SGML or XML into a
|
||||
// Response object from the given io.Reader
|
||||
//
|
||||
// It is commonly used as part of Client.Request(), but may be used on its own
|
||||
// to parse already-downloaded OFX files (such as those from 'Web Connect'). It
|
||||
// performs version autodetection if it can and attempts to be as forgiving as
|
||||
// possible about the input format.
|
||||
func ParseResponse(reader io.Reader) (*Response, error) {
|
||||
resp, err := DecodeResponse(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = resp.Valid()
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// DecodeResponse parses an OFX response in SGML or XML into a Response object
|
||||
// from the given io.Reader
|
||||
func DecodeResponse(reader io.Reader) (*Response, error) {
|
||||
var or Response
|
||||
|
||||
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
|
||||
@@ -319,9 +340,6 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
||||
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != SignonRs.String() {
|
||||
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
||||
}
|
||||
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var messageSlices = map[string]*[]Message{
|
||||
SignupRs.String(): &or.Signup,
|
||||
@@ -359,3 +377,131 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Valid returns whether the Response is valid according to the OFX spec
|
||||
func (or *Response) Valid() (bool, error) {
|
||||
var errs errInvalid
|
||||
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||
errs.AddErr(err)
|
||||
}
|
||||
for _, messageSet := range [][]Message{
|
||||
or.Signup,
|
||||
or.Bank,
|
||||
or.CreditCard,
|
||||
or.Loan,
|
||||
or.InvStmt,
|
||||
or.InterXfer,
|
||||
or.WireXfer,
|
||||
or.Billpay,
|
||||
or.Email,
|
||||
or.SecList,
|
||||
or.PresDir,
|
||||
or.PresDlv,
|
||||
or.Prof,
|
||||
or.Image,
|
||||
} {
|
||||
for _, message := range messageSet {
|
||||
if ok, err := message.Valid(or.Version); !ok {
|
||||
errs.AddErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
err := errs.ErrOrNil()
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// Marshal this Response into its SGML/XML representation held in a bytes.Buffer
|
||||
//
|
||||
// If error is non-nil, this bytes.Buffer is ready to be sent to an OFX client
|
||||
func (or *Response) Marshal() (*bytes.Buffer, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
// Write the header appropriate to our version
|
||||
writeHeader(&b, or.Version, false)
|
||||
|
||||
encoder := xml.NewEncoder(&b)
|
||||
encoder.Indent("", " ")
|
||||
|
||||
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
||||
|
||||
if err := encoder.EncodeToken(ofxElement); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||
return nil, err
|
||||
}
|
||||
signonMsgSet := xml.StartElement{Name: xml.Name{Local: SignonRs.String()}}
|
||||
if err := encoder.EncodeToken(signonMsgSet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Encode(&or.Signon); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.EncodeToken(signonMsgSet.End()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messageSets := []struct {
|
||||
Messages []Message
|
||||
Type messageType
|
||||
}{
|
||||
{or.Signup, SignupRs},
|
||||
{or.Bank, BankRs},
|
||||
{or.CreditCard, CreditCardRs},
|
||||
{or.Loan, LoanRs},
|
||||
{or.InvStmt, InvStmtRs},
|
||||
{or.InterXfer, InterXferRs},
|
||||
{or.WireXfer, WireXferRs},
|
||||
{or.Billpay, BillpayRs},
|
||||
{or.Email, EmailRs},
|
||||
{or.SecList, SecListRs},
|
||||
{or.PresDir, PresDirRs},
|
||||
{or.PresDlv, PresDlvRs},
|
||||
{or.Prof, ProfRs},
|
||||
{or.Image, ImageRs},
|
||||
}
|
||||
for _, set := range messageSets {
|
||||
if err := encodeMessageSet(encoder, set.Messages, set.Type, or.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := encoder.Flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
// errInvalid represents validation failures while parsing an OFX response
|
||||
// If an institution returns slightly malformed data, ParseResponse will return a best-effort parsed response and a validation error.
|
||||
type errInvalid []error
|
||||
|
||||
func (e errInvalid) Error() string {
|
||||
var errStrings []string
|
||||
for _, err := range e {
|
||||
errStrings = append(errStrings, err.Error())
|
||||
}
|
||||
return fmt.Sprintf("Validation failed: %s", strings.Join(errStrings, "; "))
|
||||
}
|
||||
|
||||
func (e *errInvalid) AddErr(err error) {
|
||||
if err != nil {
|
||||
if errs, ok := err.(errInvalid); ok {
|
||||
*e = append(*e, errs...)
|
||||
} else {
|
||||
*e = append(*e, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e errInvalid) ErrOrNil() error {
|
||||
if len(e) > 0 {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
180
response_test.go
180
response_test.go
@@ -1,13 +1,15 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/aclindsa/xml"
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
|
||||
// Attempt to find a method on the provided Value called 'Equal' which is a
|
||||
@@ -132,27 +134,191 @@ func checkEqual(t *testing.T, fieldName string, expected, actual reflect.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func checkResponsesEqual(t *testing.T, expected, actual *ofxgo.Response) {
|
||||
func checkResponsesEqual(t *testing.T, expected, actual *Response) {
|
||||
checkEqual(t, "", reflect.ValueOf(expected), reflect.ValueOf(actual))
|
||||
}
|
||||
|
||||
func checkResponseRoundTrip(t *testing.T, response *Response) {
|
||||
b, err := response.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error re-marshaling OFX response: %s\n", err)
|
||||
}
|
||||
roundtripped, err := ParseResponse(b)
|
||||
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.
|
||||
func TestValidSamples(t *testing.T) {
|
||||
fn := func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
} else if filepath.Ext(path) != ".ofx" {
|
||||
} else if ext := filepath.Ext(path); ext != ".ofx" && ext != ".qfx" {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error opening %s: %s\n", path, err)
|
||||
}
|
||||
_, err = ofxgo.ParseResponse(file)
|
||||
response, err := ParseResponse(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error parsing OFX response in %s: %s\n", path, err)
|
||||
}
|
||||
checkResponseRoundTrip(t, response)
|
||||
return nil
|
||||
}
|
||||
filepath.Walk("samples/valid_responses", fn)
|
||||
filepath.Walk("samples/busted_responses", fn)
|
||||
}
|
||||
|
||||
func TestInvalidResponse(t *testing.T) {
|
||||
// in this example, the severity is invalid due to mixed upper and lower case letters
|
||||
const invalidResponse = `
|
||||
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>
|
||||
`
|
||||
const expectedErr = "Validation failed: Invalid STATUS>SEVERITY; Invalid STATUS>SEVERITY"
|
||||
|
||||
t.Run("parse response", func(t *testing.T) {
|
||||
resp, err := ParseResponse(bytes.NewReader([]byte(invalidResponse)))
|
||||
expectedErr := "Validation failed: Invalid STATUS>SEVERITY; Invalid STATUS>SEVERITY"
|
||||
if err == nil {
|
||||
t.Fatalf("ParseResponse should fail with %q, found nil", expectedErr)
|
||||
}
|
||||
if _, ok := err.(errInvalid); !ok {
|
||||
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) {
|
||||
resp, err := ParseResponse(bytes.NewReader(nil))
|
||||
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) {
|
||||
resp, err := DecodeResponse(bytes.NewReader([]byte(invalidResponse)))
|
||||
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)
|
||||
}
|
||||
if _, ok := err.(errInvalid); !ok {
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrInvalidError(t *testing.T) {
|
||||
expectedErr := `Validation failed: A; B; C`
|
||||
actualErr := errInvalid{
|
||||
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) {
|
||||
var errs errInvalid
|
||||
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) {
|
||||
var errs errInvalid
|
||||
errs.AddErr(errors.New("some error"))
|
||||
|
||||
})
|
||||
|
||||
t.Run("adding the same type should flatten the errors", func(t *testing.T) {
|
||||
var errs errInvalid
|
||||
errs.AddErr(errInvalid{
|
||||
errors.New("A"),
|
||||
errors.New("B"),
|
||||
})
|
||||
errs.AddErr(errInvalid{
|
||||
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) {
|
||||
var errs errInvalid
|
||||
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.")
|
||||
}
|
||||
if _, ok := err.(errInvalid); !ok {
|
||||
t.Fatalf("Expected err to be of type errInvalid, found: %T", err)
|
||||
}
|
||||
errInv := err.(errInvalid)
|
||||
if len(errInv) != 1 || errInv[0] != someError {
|
||||
t.Errorf("Expected ErrOrNil to return itself, found: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
59
samples/busted_responses/bmo_v102__no_header_newline.qfx
Normal file
59
samples/busted_responses/bmo_v102__no_header_newline.qfx
Normal file
@@ -0,0 +1,59 @@
|
||||
OFXHEADER:100
|
||||
DATA:OFXSGML
|
||||
VERSION:102
|
||||
SECURITY:NONE
|
||||
ENCODING:USASCII
|
||||
CHARSET:1252
|
||||
COMPRESSION:NONE
|
||||
OLDFILEUID:NONE
|
||||
NEWFILEUID:NONE
|
||||
<OFX>
|
||||
<SIGNONMSGSRSV1>
|
||||
<SONRS>
|
||||
<STATUS>
|
||||
<CODE>0
|
||||
<SEVERITY>INFO
|
||||
<MESSAGE>OK
|
||||
</STATUS>
|
||||
<DTSERVER>20181202184906.217[-5:EDT]
|
||||
<USERKEY>SJLDF802DV09DF80
|
||||
<LANGUAGE>ENG
|
||||
<INTU.BID>00017
|
||||
</SONRS>
|
||||
</SIGNONMSGSRSV1>
|
||||
<CREDITCARDMSGSRSV1>
|
||||
<CCSTMTTRNRS>
|
||||
<TRNUID>1
|
||||
<STATUS>
|
||||
<CODE>0
|
||||
<SEVERITY>INFO
|
||||
<MESSAGE>OK
|
||||
</STATUS>
|
||||
<CCSTMTRS>
|
||||
<CURDEF>CAD
|
||||
<CCACCTFROM>
|
||||
<ACCTID>2380370270281083
|
||||
</CCACCTFROM>
|
||||
<BANKTRANLIST>
|
||||
<DTSTART>20181202184905.909[-5:EDT]
|
||||
<DTEND>20181202184905.909[-5:EDT]
|
||||
<STMTTRN>
|
||||
<TRNTYPE>CREDIT
|
||||
<DTPOSTED>20181030000000.000[-5:EDT]
|
||||
<TRNAMT>2042.24
|
||||
<FITID>2380370270281083201810302054456
|
||||
<NAME>PAYMENT RECEIVED - THANK YOU
|
||||
</STMTTRN>
|
||||
</BANKTRANLIST>
|
||||
<LEDGERBAL>
|
||||
<BALAMT>-552.63
|
||||
<DTASOF>20181202184906.217[-5:EDT]
|
||||
</LEDGERBAL>
|
||||
<AVAILBAL>
|
||||
<BALAMT>-552.63
|
||||
<DTASOF>20181202184906.217[-5:EDT]
|
||||
</AVAILBAL>
|
||||
</CCSTMTRS>
|
||||
</CCSTMTTRNRS>
|
||||
</CREDITCARDMSGSRSV1>
|
||||
</OFX>
|
||||
40
seclist.go
40
seclist.go
@@ -221,6 +221,7 @@ func (i StockInfo) SecurityType() string {
|
||||
// SecurityList is a container for Security objects containaing information
|
||||
// about securities
|
||||
type SecurityList struct {
|
||||
XMLName xml.Name `xml:"SECLIST"`
|
||||
Securities []Security
|
||||
}
|
||||
|
||||
@@ -290,3 +291,42 @@ func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalXML handles marshalling a SecurityList to an SGML/XML string
|
||||
func (r *SecurityList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
secListElement := xml.StartElement{Name: xml.Name{Local: "SECLIST"}}
|
||||
if err := e.EncodeToken(secListElement); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range r.Securities {
|
||||
start := xml.StartElement{Name: xml.Name{Local: s.SecurityType()}}
|
||||
switch sec := s.(type) {
|
||||
case DebtInfo:
|
||||
if err := e.EncodeElement(&sec, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case MFInfo:
|
||||
if err := e.EncodeElement(&sec, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OptInfo:
|
||||
if err := e.EncodeElement(&sec, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case OtherInfo:
|
||||
if err := e.EncodeElement(&sec, start); err != nil {
|
||||
return err
|
||||
}
|
||||
case StockInfo:
|
||||
if err := e.EncodeElement(&sec, start); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("Invalid SECLIST child type: " + sec.SecurityType())
|
||||
}
|
||||
}
|
||||
if err := e.EncodeToken(secListElement.End()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalInvalidSignons(t *testing.T) {
|
||||
var client = ofxgo.Client{
|
||||
var client = BasicClient{
|
||||
AppID: "OFXGO",
|
||||
AppVer: "0001",
|
||||
SpecVersion: ofxgo.OfxVersion203,
|
||||
SpecVersion: OfxVersion203,
|
||||
}
|
||||
|
||||
var request ofxgo.Request
|
||||
var request Request
|
||||
request.Signon.UserID = "myusername"
|
||||
request.Signon.UserPass = "Pa$$word"
|
||||
request.Signon.Org = "BNK"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -37,27 +36,27 @@ func TestMarshalAcctInfoRequest(t *testing.T) {
|
||||
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
|
||||
var client = ofxgo.Client{
|
||||
var client = BasicClient{
|
||||
AppID: "OFXGO",
|
||||
AppVer: "0001",
|
||||
SpecVersion: ofxgo.OfxVersion203,
|
||||
SpecVersion: OfxVersion203,
|
||||
}
|
||||
|
||||
var request ofxgo.Request
|
||||
var request Request
|
||||
request.Signon.UserID = "myusername"
|
||||
request.Signon.UserPass = "Pa$$word"
|
||||
request.Signon.Org = "BNK"
|
||||
request.Signon.Fid = "1987"
|
||||
|
||||
acctInfoRequest := ofxgo.AcctInfoRequest{
|
||||
acctInfoRequest := AcctInfoRequest{
|
||||
TrnUID: "e3ad9bda-38fa-4e5b-8099-1bd567ddef7a",
|
||||
DtAcctUp: *ofxgo.NewDate(2015, 12, 21, 18, 29, 45, 0, EST),
|
||||
DtAcctUp: *NewDate(2015, 12, 21, 18, 29, 45, 0, EST),
|
||||
}
|
||||
request.Signup = append(request.Signup, &acctInfoRequest)
|
||||
|
||||
request.SetClientFields(&client)
|
||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||
request.Signon.DtClient = *ofxgo.NewDate(2016, 1, 15, 11, 23, 0, 0, EST)
|
||||
request.Signon.DtClient = *NewDate(2016, 1, 15, 11, 23, 0, 0, EST)
|
||||
|
||||
marshalCheckRequest(t, &request, expectedString)
|
||||
}
|
||||
@@ -110,38 +109,38 @@ func TestUnmarshalAcctInfoResponse(t *testing.T) {
|
||||
</ACCTINFOTRNRS>
|
||||
</SIGNUPMSGSRSV1>
|
||||
</OFX>`)
|
||||
var expected ofxgo.Response
|
||||
var expected Response
|
||||
|
||||
expected.Version = ofxgo.OfxVersion203
|
||||
expected.Version = OfxVersion203
|
||||
expected.Signon.Status.Code = 0
|
||||
expected.Signon.Status.Severity = "INFO"
|
||||
expected.Signon.DtServer = *ofxgo.NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
||||
expected.Signon.DtServer = *NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
||||
expected.Signon.Language = "ENG"
|
||||
expected.Signon.DtProfUp = ofxgo.NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
||||
expected.Signon.DtAcctUp = ofxgo.NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
||||
expected.Signon.DtProfUp = NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
||||
expected.Signon.DtAcctUp = NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
||||
expected.Signon.Org = "BNK"
|
||||
expected.Signon.Fid = "1987"
|
||||
|
||||
bankacctinfo := ofxgo.BankAcctInfo{
|
||||
BankAcctFrom: ofxgo.BankAcct{
|
||||
bankacctinfo := BankAcctInfo{
|
||||
BankAcctFrom: BankAcct{
|
||||
BankID: "8367556009",
|
||||
AcctID: "000999847",
|
||||
AcctType: ofxgo.AcctTypeMoneyMrkt,
|
||||
AcctType: AcctTypeMoneyMrkt,
|
||||
},
|
||||
SupTxDl: true,
|
||||
XferSrc: true,
|
||||
XferDest: true,
|
||||
SvcStatus: ofxgo.SvcStatusActive,
|
||||
SvcStatus: SvcStatusActive,
|
||||
}
|
||||
|
||||
acctInfoResponse := ofxgo.AcctInfoResponse{
|
||||
acctInfoResponse := AcctInfoResponse{
|
||||
TrnUID: "10938754",
|
||||
Status: ofxgo.Status{
|
||||
Status: Status{
|
||||
Code: 0,
|
||||
Severity: "INFO",
|
||||
},
|
||||
DtAcctUp: *ofxgo.NewDateGMT(2005, 2, 28, 0, 0, 0, 0),
|
||||
AcctInfo: []ofxgo.AcctInfo{{
|
||||
DtAcctUp: *NewDateGMT(2005, 2, 28, 0, 0, 0, 0),
|
||||
AcctInfo: []AcctInfo{{
|
||||
Desc: "Personal Checking",
|
||||
Phone: "888-222-5827",
|
||||
BankAcctInfo: &bankacctinfo,
|
||||
@@ -149,10 +148,11 @@ func TestUnmarshalAcctInfoResponse(t *testing.T) {
|
||||
}
|
||||
expected.Signup = append(expected.Signup, &acctInfoResponse)
|
||||
|
||||
response, err := ofxgo.ParseResponse(responseReader)
|
||||
response, err := ParseResponse(responseReader)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||
}
|
||||
|
||||
checkResponsesEqual(t, &expected, response)
|
||||
checkResponseRoundTrip(t, response)
|
||||
}
|
||||
|
||||
143
types_test.go
143
types_test.go
@@ -1,9 +1,8 @@
|
||||
package ofxgo_test
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/aclindsa/xml"
|
||||
"github.com/aclindsa/ofxgo"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -57,7 +56,7 @@ func unmarshalHelper(t *testing.T, input string, expected interface{}, overwritt
|
||||
}
|
||||
|
||||
func TestMarshalInt(t *testing.T) {
|
||||
var i ofxgo.Int = 927
|
||||
var i Int = 927
|
||||
marshalHelper(t, "927", &i)
|
||||
i = 0
|
||||
marshalHelper(t, "0", &i)
|
||||
@@ -66,7 +65,7 @@ func TestMarshalInt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnmarshalInt(t *testing.T) {
|
||||
var i, overwritten ofxgo.Int = -48394, 0
|
||||
var i, overwritten Int = -48394, 0
|
||||
unmarshalHelper(t, "-48394", &i, &overwritten)
|
||||
i = 0
|
||||
unmarshalHelper(t, "0", &i, &overwritten)
|
||||
@@ -78,7 +77,7 @@ func TestUnmarshalInt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalAmount(t *testing.T) {
|
||||
var a ofxgo.Amount
|
||||
var a Amount
|
||||
|
||||
a.SetFrac64(8, 1)
|
||||
marshalHelper(t, "8", &a)
|
||||
@@ -95,13 +94,13 @@ func TestMarshalAmount(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnmarshalAmount(t *testing.T) {
|
||||
var a, overwritten ofxgo.Amount
|
||||
var a, overwritten Amount
|
||||
|
||||
// Amount/big.Rat needs a special equality test because reflect.DeepEqual
|
||||
// doesn't always return equal for two values that big.Rat.Cmp() does
|
||||
eq := func(a, b interface{}) bool {
|
||||
if amountA, ok := a.(*ofxgo.Amount); ok {
|
||||
if amountB, ok2 := b.(*ofxgo.Amount); ok2 {
|
||||
if amountA, ok := a.(*Amount); ok {
|
||||
if amountB, ok2 := b.(*Amount); ok2 {
|
||||
return amountA.Cmp(&amountB.Rat) == 0
|
||||
}
|
||||
}
|
||||
@@ -127,18 +126,18 @@ func TestUnmarshalAmount(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAmountEqual(t *testing.T) {
|
||||
assertEq := func(a, b ofxgo.Amount) {
|
||||
assertEq := func(a, b Amount) {
|
||||
if !a.Equal(b) {
|
||||
t.Fatalf("Amounts should be equal but Equal returned false: %s and %s\n", a, b)
|
||||
}
|
||||
}
|
||||
assertNEq := func(a, b ofxgo.Amount) {
|
||||
assertNEq := func(a, b Amount) {
|
||||
if a.Equal(b) {
|
||||
t.Fatalf("Amounts should not be equal but Equal returned true: %s and %s\n", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
var a, b ofxgo.Amount
|
||||
var a, b Amount
|
||||
a.SetInt64(-19487135)
|
||||
b.SetInt64(-19487135)
|
||||
assertEq(a, b)
|
||||
@@ -154,7 +153,7 @@ func TestAmountEqual(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalDate(t *testing.T) {
|
||||
var d *ofxgo.Date
|
||||
var d *Date
|
||||
UTC := time.FixedZone("UTC", 0)
|
||||
GMT_nodesc := time.FixedZone("", 0)
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
@@ -162,35 +161,35 @@ func TestMarshalDate(t *testing.T) {
|
||||
IST := time.FixedZone("IST", (5*60+30)*60)
|
||||
NST := time.FixedZone("NST", -(3*60+30)*60)
|
||||
|
||||
d = ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
marshalHelper(t, "20170314150926.053[0:GMT]", d)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
||||
marshalHelper(t, "20170314150926.053[5.75:NPT]", d)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||
marshalHelper(t, "20170314150926.053[-5:EST]", d)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, UTC)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, UTC)
|
||||
marshalHelper(t, "20170314150926.053[0:UTC]", d)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
||||
marshalHelper(t, "20170314150926.053[5.50:IST]", d)
|
||||
d = ofxgo.NewDate(9999, 11, 1, 23, 59, 59, 1000, EST)
|
||||
d = NewDate(9999, 11, 1, 23, 59, 59, 1000, EST)
|
||||
marshalHelper(t, "99991101235959.000[-5:EST]", d)
|
||||
d = ofxgo.NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
||||
d = NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
||||
marshalHelper(t, "00000101000000.000[5.50:IST]", d)
|
||||
d = &ofxgo.Date{Time: time.Unix(0, 0).In(UTC)}
|
||||
d = &Date{Time: time.Unix(0, 0).In(UTC)}
|
||||
marshalHelper(t, "19700101000000.000[0:UTC]", d)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
||||
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
||||
marshalHelper(t, "20170314000026.053[-5:EST]", d)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
||||
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
||||
marshalHelper(t, "20170314000026.053[-3.50:NST]", d)
|
||||
|
||||
// Time zone without textual description
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT_nodesc)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT_nodesc)
|
||||
marshalHelper(t, "20170314150926.053[0]", d)
|
||||
}
|
||||
|
||||
func TestUnmarshalDate(t *testing.T) {
|
||||
var d *ofxgo.Date
|
||||
var overwritten ofxgo.Date
|
||||
var d *Date
|
||||
var overwritten Date
|
||||
GMT := time.FixedZone("GMT", 0)
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
NPT := time.FixedZone("NPT", (5*60+45)*60)
|
||||
@@ -199,8 +198,8 @@ func TestUnmarshalDate(t *testing.T) {
|
||||
NST_nodesc := time.FixedZone("", -(3*60+30)*60)
|
||||
|
||||
eq := func(a, b interface{}) bool {
|
||||
if dateA, ok := a.(*ofxgo.Date); ok {
|
||||
if dateB, ok2 := b.(*ofxgo.Date); ok2 {
|
||||
if dateA, ok := a.(*Date); ok {
|
||||
if dateB, ok2 := b.(*Date); ok2 {
|
||||
return dateA.Equal(*dateB)
|
||||
}
|
||||
}
|
||||
@@ -208,14 +207,14 @@ func TestUnmarshalDate(t *testing.T) {
|
||||
}
|
||||
|
||||
// Ensure omitted fields default to the correct values
|
||||
d = ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq)
|
||||
unmarshalHelper2(t, "20170314150926.053", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 0, 0, GMT)
|
||||
d = NewDate(2017, 3, 14, 0, 0, 0, 0, GMT)
|
||||
unmarshalHelper2(t, "20170314", d, &overwritten, eq)
|
||||
|
||||
// Ensure all signs on time zone offsets are properly handled
|
||||
d = ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
unmarshalHelper2(t, "20170314150926.053[0:GMT]", d, &overwritten, eq)
|
||||
unmarshalHelper2(t, "20170314150926.053[+0:GMT]", d, &overwritten, eq)
|
||||
unmarshalHelper2(t, "20170314150926.053[-0:GMT]", d, &overwritten, eq)
|
||||
@@ -223,38 +222,38 @@ func TestUnmarshalDate(t *testing.T) {
|
||||
unmarshalHelper2(t, "20170314150926.053[+0]", d, &overwritten, eq)
|
||||
unmarshalHelper2(t, "20170314150926.053[-0]", d, &overwritten, eq)
|
||||
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
||||
unmarshalHelper2(t, "20170314150926.053[5.75:NPT]", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||
unmarshalHelper2(t, "20170314150926.053[-5:EST]", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||
unmarshalHelper2(t, "20170314150926.053[0:GMT]", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
||||
unmarshalHelper2(t, "20170314150926.053[5.50:IST]", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
||||
d = NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
||||
unmarshalHelper2(t, "20181101235958.000[-5:EST]", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
||||
d = NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
||||
unmarshalHelper2(t, "00000101000000.000[5.50:IST]", d, &overwritten, eq)
|
||||
d = &ofxgo.Date{Time: time.Unix(0, 0).In(GMT)}
|
||||
d = &Date{Time: time.Unix(0, 0).In(GMT)}
|
||||
unmarshalHelper2(t, "19700101000000.000[0:GMT]", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
||||
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
||||
unmarshalHelper2(t, "20170314000026.053[-5:EST]", d, &overwritten, eq)
|
||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
||||
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
||||
unmarshalHelper2(t, "20170314000026.053[-3.50:NST]", d, &overwritten, eq)
|
||||
|
||||
// Autopopulate zone without textual description for GMT
|
||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||
unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq)
|
||||
// but not for others:
|
||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST_nodesc)
|
||||
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST_nodesc)
|
||||
unmarshalHelper2(t, "20170314000026.053[-3.50]", d, &overwritten, eq)
|
||||
|
||||
// Make sure we handle poorly-formatted dates (from Vanguard)
|
||||
d = ofxgo.NewDate(2016, 12, 7, 16, 0, 0, 0, EST)
|
||||
d = NewDate(2016, 12, 7, 16, 0, 0, 0, EST)
|
||||
unmarshalHelper2(t, "20161207160000.000[-5:EST]610900.500[-9:BST]", d, &overwritten, eq) // extra part intentionally different to ensure the first timezone is parsed
|
||||
|
||||
// Make sure we properly handle ending newlines
|
||||
d = ofxgo.NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
||||
d = NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
||||
unmarshalHelper2(t, "20181101235958.000[-5:EST]\n", d, &overwritten, eq)
|
||||
unmarshalHelper2(t, "20181101235958.000[-5:EST]\n\t", d, &overwritten, eq)
|
||||
}
|
||||
@@ -263,23 +262,23 @@ func TestDateEqual(t *testing.T) {
|
||||
GMT := time.FixedZone("GMT", 0)
|
||||
EST := time.FixedZone("EST", -5*60*60)
|
||||
|
||||
assertEq := func(a, b *ofxgo.Date) {
|
||||
assertEq := func(a, b *Date) {
|
||||
if !a.Equal(*b) {
|
||||
t.Fatalf("Dates should be equal but Equal returned false: %s and %s\n", *a, *b)
|
||||
}
|
||||
}
|
||||
assertNEq := func(a, b *ofxgo.Date) {
|
||||
assertNEq := func(a, b *Date) {
|
||||
if a.Equal(*b) {
|
||||
t.Fatalf("Dates should not be equal but Equal returned true: %s and %s\n", *a, *b)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure omitted fields default to the correct values
|
||||
gmt1 := ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
gmt2 := ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||
est1 := ofxgo.NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000, EST)
|
||||
est2 := ofxgo.NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000+1, EST)
|
||||
est3 := ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||
gmt1 := NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||
gmt2 := NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||
est1 := NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000, EST)
|
||||
est2 := NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000+1, EST)
|
||||
est3 := NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||
|
||||
assertEq(gmt1, gmt2)
|
||||
assertEq(gmt2, gmt1)
|
||||
@@ -291,7 +290,7 @@ func TestDateEqual(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalString(t *testing.T) {
|
||||
var s ofxgo.String = ""
|
||||
var s String = ""
|
||||
marshalHelper(t, "", &s)
|
||||
s = "foo&bar"
|
||||
marshalHelper(t, "foo&bar", &s)
|
||||
@@ -302,7 +301,7 @@ func TestMarshalString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnmarshalString(t *testing.T) {
|
||||
var s, overwritten ofxgo.String = "", ""
|
||||
var s, overwritten String = "", ""
|
||||
unmarshalHelper(t, "", &s, &overwritten)
|
||||
s = "foo&bar"
|
||||
unmarshalHelper(t, "foo&bar", &s, &overwritten)
|
||||
@@ -318,14 +317,14 @@ func TestUnmarshalString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalBoolean(t *testing.T) {
|
||||
var b ofxgo.Boolean = true
|
||||
var b Boolean = true
|
||||
marshalHelper(t, "Y", &b)
|
||||
b = false
|
||||
marshalHelper(t, "N", &b)
|
||||
}
|
||||
|
||||
func TestUnmarshalBoolean(t *testing.T) {
|
||||
var b, overwritten ofxgo.Boolean = true, false
|
||||
var b, overwritten Boolean = true, false
|
||||
unmarshalHelper(t, "Y", &b, &overwritten)
|
||||
b = false
|
||||
unmarshalHelper(t, "N", &b, &overwritten)
|
||||
@@ -335,12 +334,12 @@ func TestUnmarshalBoolean(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalUID(t *testing.T) {
|
||||
var u ofxgo.UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
||||
var u UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
||||
marshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u)
|
||||
}
|
||||
|
||||
func TestUnmarshalUID(t *testing.T) {
|
||||
var u, overwritten ofxgo.UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04", ""
|
||||
var u, overwritten UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04", ""
|
||||
unmarshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u, &overwritten)
|
||||
// Make sure stray newlines are handled properly
|
||||
u = "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab"
|
||||
@@ -349,7 +348,7 @@ func TestUnmarshalUID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUIDRecommendedFormat(t *testing.T) {
|
||||
var u ofxgo.UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
||||
var u UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
||||
if ok, err := u.RecommendedFormat(); !ok || err != nil {
|
||||
t.Fatalf("UID unexpectedly failed validation\n")
|
||||
}
|
||||
@@ -368,7 +367,7 @@ func TestUIDRecommendedFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUIDValid(t *testing.T) {
|
||||
var u ofxgo.UID = ""
|
||||
var u UID = ""
|
||||
if ok, err := u.Valid(); ok || err == nil {
|
||||
t.Fatalf("Empty UID unexpectedly valid\n")
|
||||
}
|
||||
@@ -383,7 +382,7 @@ func TestUIDValid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRandomUID(t *testing.T) {
|
||||
uid, err := ofxgo.RandomUID()
|
||||
uid, err := RandomUID()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error when calling RandomUID: %s\n", err)
|
||||
}
|
||||
@@ -393,46 +392,46 @@ func TestRandomUID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalCurrSymbol(t *testing.T) {
|
||||
c, _ := ofxgo.NewCurrSymbol("USD")
|
||||
c, _ := NewCurrSymbol("USD")
|
||||
marshalHelper(t, "USD", &c)
|
||||
}
|
||||
|
||||
func TestUnmarshalCurrSymbol(t *testing.T) {
|
||||
var overwritten ofxgo.CurrSymbol
|
||||
c, _ := ofxgo.NewCurrSymbol("USD")
|
||||
var overwritten CurrSymbol
|
||||
c, _ := NewCurrSymbol("USD")
|
||||
unmarshalHelper(t, "USD", c, &overwritten)
|
||||
// Make sure stray newlines are handled properly
|
||||
c, _ = ofxgo.NewCurrSymbol("EUR")
|
||||
c, _ = NewCurrSymbol("EUR")
|
||||
unmarshalHelper(t, "EUR\n", c, &overwritten)
|
||||
unmarshalHelper(t, "EUR\n\t", c, &overwritten)
|
||||
}
|
||||
|
||||
func TestCurrSymbolEqual(t *testing.T) {
|
||||
usd1, _ := ofxgo.NewCurrSymbol("USD")
|
||||
usd2, _ := ofxgo.NewCurrSymbol("USD")
|
||||
usd1, _ := NewCurrSymbol("USD")
|
||||
usd2, _ := NewCurrSymbol("USD")
|
||||
if !usd1.Equal(*usd2) {
|
||||
t.Fatalf("Two \"USD\" CurrSymbols returned !Equal()\n")
|
||||
}
|
||||
xxx, _ := ofxgo.NewCurrSymbol("XXX")
|
||||
xxx, _ := NewCurrSymbol("XXX")
|
||||
if usd1.Equal(*xxx) {
|
||||
t.Fatalf("\"USD\" and \"XXX\" CurrSymbols returned Equal()\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrSymbolValid(t *testing.T) {
|
||||
var initial ofxgo.CurrSymbol
|
||||
var initial CurrSymbol
|
||||
ok, err := initial.Valid()
|
||||
if ok || err == nil {
|
||||
t.Fatalf("CurrSymbol unexpectedly returned Valid() for initial value\n")
|
||||
}
|
||||
|
||||
ars, _ := ofxgo.NewCurrSymbol("ARS")
|
||||
ars, _ := NewCurrSymbol("ARS")
|
||||
ok, err = ars.Valid()
|
||||
if !ok || err != nil {
|
||||
t.Fatalf("CurrSymbol unexpectedly returned !Valid() for \"ARS\": %s\n", err.Error())
|
||||
}
|
||||
|
||||
xxx, _ := ofxgo.NewCurrSymbol("XXX")
|
||||
xxx, _ := NewCurrSymbol("XXX")
|
||||
ok, err = xxx.Valid()
|
||||
if ok || err == nil {
|
||||
t.Fatalf("CurrSymbol unexpectedly returned Valid() for \"XXX\"\n")
|
||||
@@ -440,21 +439,21 @@ func TestCurrSymbolValid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewCurrSymbol(t *testing.T) {
|
||||
curr, err := ofxgo.NewCurrSymbol("GBP")
|
||||
curr, err := NewCurrSymbol("GBP")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err)
|
||||
}
|
||||
if curr.String() != "GBP" {
|
||||
t.Fatalf("Created CurrSymbol doesn't print \"GBP\" as string representation\n")
|
||||
}
|
||||
curr, err = ofxgo.NewCurrSymbol("AFN")
|
||||
curr, err = NewCurrSymbol("AFN")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err)
|
||||
}
|
||||
if curr.String() != "AFN" {
|
||||
t.Fatalf("Created CurrSymbol doesn't print \"AFN\" as string representation\n")
|
||||
}
|
||||
curr, err = ofxgo.NewCurrSymbol("BLAH")
|
||||
curr, err = NewCurrSymbol("BLAH")
|
||||
if err == nil {
|
||||
t.Fatalf("NewCurrSymbol didn't error on invalid currency identifier\n")
|
||||
}
|
||||
|
||||
79
vanguard_client.go
Normal file
79
vanguard_client.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// VanguardClient provides a Client implementation which handles Vanguard's
|
||||
// cookie-passing requirements. VanguardClient uses default, non-zero settings,
|
||||
// if its fields are not initialized.
|
||||
type VanguardClient struct {
|
||||
*BasicClient
|
||||
}
|
||||
|
||||
// NewVanguardClient returns a Client interface configured to handle Vanguard's
|
||||
// brand of idiosyncracy
|
||||
func NewVanguardClient(bc *BasicClient) Client {
|
||||
return &VanguardClient{bc}
|
||||
}
|
||||
|
||||
// rawRequestCookies is RawRequest with the added feature of sending cookies
|
||||
func rawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
|
||||
if !strings.HasPrefix(URL, "https://") {
|
||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", URL, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/x-ofx")
|
||||
for _, cookie := range cookies {
|
||||
request.AddCookie(cookie)
|
||||
}
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||
r.SetClientFields(c)
|
||||
|
||||
b, err := r.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := c.RawRequest(r.URL, b)
|
||||
|
||||
// Some financial institutions (cough, Vanguard, cough), require a cookie
|
||||
// to be set on the http request, or they return empty responses.
|
||||
// Fortunately, the initial response contains the cookie we need, so if we
|
||||
// detect an empty response with cookies set that didn't have any errors,
|
||||
// re-try the request while sending their cookies back to them.
|
||||
if err == nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
||||
b, err = r.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rawRequestCookies(r.URL, b, response.Cookies())
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (c *VanguardClient) Request(r *Request) (*Response, error) {
|
||||
return clientRequest(c, r)
|
||||
}
|
||||
Reference in New Issue
Block a user