1
0
mirror of https://github.com/aclindsa/moneygo.git synced 2025-06-13 21:48:39 -04:00

Split integration tests into their own package

This commit is contained in:
2017-12-09 19:55:34 -05:00
parent b907682fa3
commit e52719c3f7
25 changed files with 25 additions and 25 deletions

View File

@ -0,0 +1,77 @@
package integration_test
import (
"fmt"
"strconv"
"strings"
"testing"
)
func TestLuaAccounts(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
accounts, err := getAccounts(d.clients[0])
if err != nil {
t.Fatalf("Error getting accounts: %s", err)
}
accountids := make(Int64Slice, len(*accounts.Accounts))
for i, s := range *accounts.Accounts {
accountids[i] = s.AccountId
}
accountids.Sort()
equalityString := ""
for i := range accountids {
for j := range accountids {
if i == j {
equalityString += "true"
} else {
equalityString += "false"
}
}
}
id := d.accounts[3].AccountId
simpleLuaTest(t, d.clients[0], []LuaTest{
{"SecurityId", fmt.Sprintf("return get_accounts()[%d].SecurityId", id), strconv.FormatInt(d.accounts[3].SecurityId, 10)},
{"Security", fmt.Sprintf("return get_accounts()[%d].Security.SecurityId", id), strconv.FormatInt(d.accounts[3].SecurityId, 10)},
{"Parent", fmt.Sprintf("return get_accounts()[%d].Parent.AccountId", id), strconv.FormatInt(d.accounts[3].ParentAccountId, 10)},
{"Name", fmt.Sprintf("return get_accounts()[%d].Name", id), d.accounts[3].Name},
{"Type", fmt.Sprintf("return get_accounts()[%d].Type", id), strconv.FormatInt(int64(d.accounts[3].Type), 10)},
{"TypeName", fmt.Sprintf("return get_accounts()[%d].TypeName", id), d.accounts[3].Type.String()},
{"typename", fmt.Sprintf("return get_accounts()[%d].typename", id), strings.ToLower(d.accounts[3].Type.String())},
{"Balance()", fmt.Sprintf("return get_accounts()[%d]:Balance().Amount", id), "87.19"},
{"Balance(1)", fmt.Sprintf("return get_accounts()[%d]:Balance(date.new('2017-10-30')).Amount", id), "5.6"},
{"Balance(2)", fmt.Sprintf("return get_accounts()[%d]:Balance(date.new(2017, 10, 30), date.new('2017-11-01')).Amount", id), "81.59"},
{"__tostring", fmt.Sprintf("return get_accounts()[%d]", id), "Expenses/Groceries"},
{"__eq", `
accounts = get_accounts()
sorted = {}
for id in pairs(accounts) do
table.insert(sorted, id)
end
str = ""
table.sort(sorted)
for i,idi in ipairs(sorted) do
for j,idj in ipairs(sorted) do
if accounts[idi] == accounts[idj] then
str = str .. "true"
else
str = str .. "false"
end
end
end
return str`, equalityString},
{"get_accounts()", `
sorted = {}
for id in pairs(get_accounts()) do
table.insert(sorted, id)
end
table.sort(sorted)
str = "["
for i,id in ipairs(sorted) do
str = str .. id .. " "
end
return string.sub(str, 1, -2) .. "]"`, fmt.Sprint(accountids)},
})
})
}

View File

@ -0,0 +1,222 @@
package integration_test
import (
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"strconv"
"testing"
)
func createAccount(client *http.Client, account *models.Account) (*models.Account, error) {
var a models.Account
err := create(client, account, &a, "/v1/accounts/")
return &a, err
}
func getAccount(client *http.Client, accountid int64) (*models.Account, error) {
var a models.Account
err := read(client, &a, "/v1/accounts/"+strconv.FormatInt(accountid, 10))
if err != nil {
return nil, err
}
return &a, nil
}
func getAccounts(client *http.Client) (*models.AccountList, error) {
var al models.AccountList
err := read(client, &al, "/v1/accounts/")
if err != nil {
return nil, err
}
return &al, nil
}
func updateAccount(client *http.Client, account *models.Account) (*models.Account, error) {
var a models.Account
err := update(client, account, &a, "/v1/accounts/"+strconv.FormatInt(account.AccountId, 10))
if err != nil {
return nil, err
}
return &a, nil
}
func deleteAccount(client *http.Client, a *models.Account) error {
err := remove(client, "/v1/accounts/"+strconv.FormatInt(a.AccountId, 10))
if err != nil {
return err
}
return nil
}
func TestCreateAccount(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].accounts); i++ {
orig := data[0].accounts[i]
a := d.accounts[i]
if a.AccountId == 0 {
t.Errorf("Unable to create account: %+v", a)
}
if a.Type != orig.Type {
t.Errorf("Type doesn't match")
}
if a.Name != orig.Name {
t.Errorf("Name doesn't match")
}
}
})
}
func TestGetAccount(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].accounts); i++ {
orig := data[0].accounts[i]
curr := d.accounts[i]
a, err := getAccount(d.clients[orig.UserId], curr.AccountId)
if err != nil {
t.Fatalf("Error fetching accounts: %s\n", err)
}
if a.SecurityId != curr.SecurityId {
t.Errorf("SecurityId doesn't match")
}
if a.Type != orig.Type {
t.Errorf("Type doesn't match")
}
if a.Name != orig.Name {
t.Errorf("Name doesn't match")
}
}
})
}
func TestGetAccounts(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
al, err := getAccounts(d.clients[0])
if err != nil {
t.Fatalf("Error fetching accounts: %s\n", err)
}
numaccounts := 0
foundIds := make(map[int64]bool)
for i := 0; i < len(data[0].accounts); i++ {
orig := data[0].accounts[i]
curr := d.accounts[i]
if curr.UserId != d.users[0].UserId {
continue
}
numaccounts += 1
found := false
for _, a := range *al.Accounts {
if orig.Name == a.Name && orig.Type == a.Type && a.ExternalAccountId == orig.ExternalAccountId && d.securities[orig.SecurityId].SecurityId == a.SecurityId && ((orig.ParentAccountId == -1 && a.ParentAccountId == -1) || d.accounts[orig.ParentAccountId].AccountId == a.ParentAccountId) {
if _, ok := foundIds[a.AccountId]; ok {
continue
}
foundIds[a.AccountId] = true
found = true
break
}
}
if !found {
t.Errorf("Unable to find matching account: %+v", orig)
}
}
if numaccounts != len(*al.Accounts) {
t.Fatalf("Expected %d accounts, received %d", numaccounts, len(*al.Accounts))
}
})
}
func TestUpdateAccount(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].accounts); i++ {
orig := data[0].accounts[i]
curr := d.accounts[i]
curr.Name = "blah"
curr.Type = models.Payable
for _, s := range d.securities {
if s.UserId == curr.UserId {
curr.SecurityId = s.SecurityId
break
}
}
a, err := updateAccount(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error updating account: %s\n", err)
}
if a.AccountId != curr.AccountId {
t.Errorf("AccountId doesn't match")
}
if a.Type != curr.Type {
t.Errorf("Type doesn't match")
}
if a.Name != curr.Name {
t.Errorf("Name doesn't match")
}
if a.SecurityId != curr.SecurityId {
t.Errorf("SecurityId doesn't match")
}
}
orig := data[0].accounts[0]
curr := d.accounts[0]
curr.ParentAccountId = curr.AccountId
a, err := updateAccount(d.clients[orig.UserId], &curr)
if err == nil {
t.Fatalf("Expected error updating account to be own parent: %+v\n", a)
}
orig = data[0].accounts[0]
curr = d.accounts[0]
curr.ParentAccountId = 999999
a, err = updateAccount(d.clients[orig.UserId], &curr)
if err == nil {
t.Fatalf("Expected error updating account with invalid parent: %+v\n", a)
}
orig = data[0].accounts[0]
curr = d.accounts[0]
child := d.accounts[1]
curr.ParentAccountId = child.AccountId
a, err = updateAccount(d.clients[orig.UserId], &curr)
if err == nil {
t.Fatalf("Expected error updating account with circular parent relationship: %+v\n", a)
}
})
}
func TestDeleteAccount(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].accounts); i++ {
orig := data[0].accounts[i]
curr := d.accounts[i]
err := deleteAccount(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error deleting account: %s\n", err)
}
_, err = getAccount(d.clients[orig.UserId], curr.AccountId)
if err == nil {
t.Fatalf("Expected error fetching deleted account")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error fetching deleted account: %s", herr)
}
} else {
t.Fatalf("Unexpected error fetching deleted account")
}
}
})
}

View File

@ -0,0 +1,40 @@
package integration_test
import (
"fmt"
"testing"
)
func TestLuaBalances(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
accountid := d.accounts[3].AccountId
symbol := d.securities[data[0].accounts[3].SecurityId].Symbol
simpleLuaTest(t, d.clients[0], []LuaTest{
{"Account:Balance()", fmt.Sprintf("return get_accounts()[%d]:Balance()", accountid), symbol + " 87.19"},
{"Account:Balance(1)", fmt.Sprintf("return get_accounts()[%d]:Balance(date.new('2017-10-30')).Amount", accountid), "5.6"},
{"Account:Balance(2)", fmt.Sprintf("return get_accounts()[%d]:Balance(date.new(2017, 10, 30), date.new('2017-11-01')).Amount", accountid), "81.59"},
{"Security", fmt.Sprintf("return get_accounts()[%d]:Balance().Security.Symbol", accountid), symbol},
{"__eq", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new(2017, 10, 30)) == (act:Balance(date.new('2017-10-29')) + 0.0)", accountid), "true"},
{"not __eq", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new(2017, 10, 30)) == act:Balance(date.new('2017-11-01'))", accountid), "false"},
{"__lt", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new(2017, 10, 14)) < act:Balance(date.new('2017-10-16'))", accountid), "true"},
{"not __lt", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new(2017, 11, 01)) < act:Balance(date.new('2017-10-16'))", accountid), "false"},
{"__le", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new(2017, 10, 14)) <= act:Balance(date.new('2017-10-16'))", accountid), "true"},
{"__le (=)", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new(2017, 10, 16)) <= act:Balance(date.new('2017-10-17'))", accountid), "true"},
{"not __le", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new(2017, 11, 01)) <= act:Balance(date.new('2017-10-16'))", accountid), "false"},
{"__add", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) + act:Balance(date.new(2017, 10, 30), date.new('2017-11-01'))", accountid), symbol + " 87.19"},
{"__add number", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) + 9", accountid), symbol + " 14.60"},
{"__add to number", fmt.Sprintf("act = get_accounts()[%d]; return 5.489 + act:Balance(date.new(2017, 10, 30), date.new('2017-11-01'))", accountid), symbol + " 87.08"},
{"__sub", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) - act:Balance(date.new(2017, 10, 30), date.new('2017-11-01'))", accountid), symbol + " -75.99"},
{"__sub number", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) - 5", accountid), symbol + " 0.60"},
{"__sub from number", fmt.Sprintf("act = get_accounts()[%d]; return 100 - act:Balance(date.new(2017, 10, 30), date.new('2017-11-01'))", accountid), symbol + " 18.41"},
{"__mul", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) * act:Balance(date.new(2017, 10, 30), date.new('2017-11-01'))", accountid), symbol + " 456.90"},
{"__mul number", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) * 5", accountid), symbol + " 28.00"},
{"__mul with number", fmt.Sprintf("act = get_accounts()[%d]; return 11.1111 * act:Balance(date.new('2017-10-30')) * 5", accountid), symbol + " 311.11"},
{"__div", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) / act:Balance(date.new(2017, 10, 30), date.new('2017-11-01'))", accountid), symbol + " 0.07"},
{"__div number", fmt.Sprintf("act = get_accounts()[%d]; return act:Balance(date.new('2017-10-30')) / 5", accountid), symbol + " 1.12"},
{"__div with number", fmt.Sprintf("act = get_accounts()[%d]; return 11.1111 / act:Balance(date.new('2017-10-30'))", accountid), symbol + " 1.98"},
{"__unm", fmt.Sprintf("act = get_accounts()[%d]; return -act:Balance(date.new('2017-10-30'))", accountid), symbol + " -5.60"},
})
})
}

View File

@ -0,0 +1,272 @@
package integration_test
import (
"bytes"
"encoding/json"
"github.com/aclindsa/moneygo/internal/config"
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"github.com/aclindsa/moneygo/internal/store/db"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"testing"
)
var server *httptest.Server
func Delete(client *http.Client, url string) (*http.Response, error) {
request, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return nil, err
}
return client.Do(request)
}
func Put(client *http.Client, url string, contentType string, body io.Reader) (*http.Response, error) {
request, err := http.NewRequest(http.MethodPut, url, body)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", contentType)
return client.Do(request)
}
type TransactType interface {
Read(string) error
}
func create(client *http.Client, input, output TransactType, urlsuffix string) error {
obj, err := json.MarshalIndent(input, "", " ")
if err != nil {
return err
}
response, err := client.Post(server.URL+urlsuffix, "application/json", bytes.NewReader(obj))
if err != nil {
return err
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return err
}
var e handlers.Error
err = (&e).Read(string(body))
if err != nil {
return err
}
if e.ErrorId != 0 || len(e.ErrorString) != 0 {
return &e
}
err = output.Read(string(body))
if err != nil {
return err
}
return nil
}
func read(client *http.Client, output TransactType, urlsuffix string) error {
response, err := client.Get(server.URL + urlsuffix)
if err != nil {
return err
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return err
}
var e handlers.Error
err = (&e).Read(string(body))
if err != nil {
return err
}
if e.ErrorId != 0 || len(e.ErrorString) != 0 {
return &e
}
err = output.Read(string(body))
if err != nil {
return err
}
return nil
}
func update(client *http.Client, input, output TransactType, urlsuffix string) error {
obj, err := json.MarshalIndent(input, "", " ")
if err != nil {
return err
}
response, err := Put(client, server.URL+urlsuffix, "application/json", bytes.NewReader(obj))
if err != nil {
return err
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return err
}
var e handlers.Error
err = (&e).Read(string(body))
if err != nil {
return err
}
if e.ErrorId != 0 || len(e.ErrorString) != 0 {
return &e
}
err = output.Read(string(body))
if err != nil {
return err
}
return nil
}
func remove(client *http.Client, urlsuffix string) error {
response, err := Delete(client, server.URL+urlsuffix)
if err != nil {
return err
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return err
}
var e handlers.Error
err = (&e).Read(string(body))
if err != nil {
return err
}
if e.ErrorId != 0 || len(e.ErrorString) != 0 {
return &e
}
return nil
}
func uploadFile(client *http.Client, filename, urlsuffix string) error {
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
filewriter, err := mw.CreateFormFile("file", filename)
if err != nil {
return err
}
if _, err := io.Copy(filewriter, file); err != nil {
return err
}
mw.Close()
response, err := client.Post(server.URL+urlsuffix, mw.FormDataContentType(), &buf)
if err != nil {
return err
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return err
}
var e handlers.Error
err = (&e).Read(string(body))
if err != nil {
return err
}
if e.ErrorId != 0 || len(e.ErrorString) != 0 {
return &e
}
return nil
}
func accountBalanceHelper(t *testing.T, client *http.Client, account *models.Account, balance string) {
t.Helper()
transactions, err := getAccountTransactions(client, account.AccountId, 0, 0, "")
if err != nil {
t.Fatalf("Couldn't fetch account transactions for '%s': %s\n", account.Name, err)
}
if transactions.EndingBalance != balance {
t.Errorf("Expected ending balance for '%s' to be '%s', but found %s\n", account.Name, balance, transactions.EndingBalance)
}
}
func RunWith(t *testing.T, d *TestData, fn TestDataFunc) {
testdata, err := d.Initialize()
if err != nil {
t.Fatalf("Failed to initialize test data: %s", err)
}
defer func() {
err := testdata.Teardown()
if err != nil {
t.Fatal(err)
}
}()
fn(t, testdata)
}
func RunTests(m *testing.M) int {
envDbType := os.Getenv("MONEYGO_TEST_DB")
var dbType config.DbType
var dsn string
switch envDbType {
case "", "sqlite", "sqlite3":
dbType = config.SQLite
dsn = ":memory:"
case "mariadb", "mysql":
dbType = config.MySQL
dsn = "root@127.0.0.1/moneygo_test&parseTime=true"
case "postgres", "postgresql":
dbType = config.Postgres
dsn = "postgres://postgres@localhost/moneygo_test"
default:
log.Fatalf("Invalid value for $MONEYGO_TEST_DB: %s\n", envDbType)
}
if envDSN := os.Getenv("MONEYGO_TEST_DSN"); len(envDSN) > 0 {
dsn = envDSN
}
db, err := db.GetStore(dbType, dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.Empty() // clear the DB tables
server = httptest.NewTLSServer(&handlers.APIHandler{Store: db})
defer server.Close()
return m.Run()
}
func TestMain(m *testing.M) {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
os.Exit(RunTests(m))
}

View File

@ -0,0 +1,31 @@
package integration_test
import (
"testing"
)
func TestLuaDates(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
simpleLuaTest(t, d.clients[0], []LuaTest{
{"Year", "return date.new('0009-01-03').Year", "9"},
{"Month", "return date.new('3999-02-01').Month", "2"},
{"Day", "return date.new('1997-12-31').Day", "31"},
{"__tostring", "return date.new('0997-12-01')", "0997-12-01"},
{"__tostring 2", "return date.new(997, 12, 1)", "0997-12-01"},
{"__tostring 3", "return date.new({year=997, month=12, day=1})", "0997-12-01"},
{"__eq", "return date.new('2017-10-05') == date.new(2017, 10, 5)", "true"},
{"(not) __eq", "return date.new('0997-12-01') == date.new('1997-12-01')", "false"},
{"__lt", "return date.new('0997-12-01') < date.new('1997-12-01')", "true"},
{"(not) __lt", "return date.new('2001-12-01') < date.new('1997-12-01')", "false"},
{"not __lt self", "return date.new('2001-12-01') < date.new('2001-12-01')", "false"},
{"__le", "return date.new('0997-12-01') <= date.new('1997-12-01')", "true"},
{"(not) __le", "return date.new(2001, 12, 1) <= date.new('1997-12-01')", "false"},
{"__le self", "return date.new('2001-12-01') <= date.new(2001, 12, 1)", "true"},
{"__add", "return date.new('2001-12-30') + date.new({year=0, month=0, day=1})", "2001-12-31"},
{"__add", "return date.new('2001-12-30') + date.new({year=0, month=0, day=2})", "2002-01-01"},
{"__sub", "return date.new('2001-12-30') - date.new({year=1, month=1, day=1})", "2000-11-29"},
{"__sub", "return date.new('2058-03-01') - date.new({year=0, month=0, day=1})", "2058-02-28"},
{"__sub", "return date.new('2058-03-31') - date.new({year=0, month=1, day=0})", "2058-02-28"},
})
})
}

View File

@ -0,0 +1,129 @@
package integration_test
import (
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"testing"
)
func importGnucash(client *http.Client, filename string) error {
return uploadFile(client, filename, "/v1/imports/gnucash")
}
func TestImportGnucash(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
// Ensure there's only one USD currency
oldDefault, err := getSecurity(d.clients[0], d.users[0].DefaultCurrency)
if err != nil {
t.Fatalf("Error fetching default security: %s\n", err)
}
d.users[0].DefaultCurrency = d.securities[0].SecurityId
if _, err := updateUser(d.clients[0], &d.users[0]); err != nil {
t.Fatalf("Error updating user: %s\n", err)
}
if err := deleteSecurity(d.clients[0], oldDefault); err != nil {
t.Fatalf("Error removing default security: %s\n", err)
}
// Import and ensure it didn't return a nasty error code
if err = importGnucash(d.clients[0], "testdata/example.gnucash"); err != nil {
t.Fatalf("Error importing from Gnucash: %s\n", err)
}
// Next, find the Expenses/Groceries account and verify it's balance
var income, equity, liabilities, expenses, salary, creditcard, groceries, cable, openingbalances *models.Account
accounts, err := getAccounts(d.clients[0])
if err != nil {
t.Fatalf("Error fetching accounts: %s\n", err)
}
for i, account := range *accounts.Accounts {
if account.Name == "Income" && account.Type == models.Income && account.ParentAccountId == -1 {
income = (*accounts.Accounts)[i]
} else if account.Name == "Equity" && account.Type == models.Equity && account.ParentAccountId == -1 {
equity = (*accounts.Accounts)[i]
} else if account.Name == "Liabilities" && account.Type == models.Liability && account.ParentAccountId == -1 {
liabilities = (*accounts.Accounts)[i]
} else if account.Name == "Expenses" && account.Type == models.Expense && account.ParentAccountId == -1 {
expenses = (*accounts.Accounts)[i]
}
}
if income == nil {
t.Fatalf("Couldn't find 'Income' account")
}
if equity == nil {
t.Fatalf("Couldn't find 'Equity' account")
}
if liabilities == nil {
t.Fatalf("Couldn't find 'Liabilities' account")
}
if expenses == nil {
t.Fatalf("Couldn't find 'Expenses' account")
}
for i, account := range *accounts.Accounts {
if account.Name == "Salary" && account.Type == models.Income && account.ParentAccountId == income.AccountId {
salary = (*accounts.Accounts)[i]
} else if account.Name == "Opening Balances" && account.Type == models.Equity && account.ParentAccountId == equity.AccountId {
openingbalances = (*accounts.Accounts)[i]
} else if account.Name == "Credit Card" && account.Type == models.Liability && account.ParentAccountId == liabilities.AccountId {
creditcard = (*accounts.Accounts)[i]
} else if account.Name == "Groceries" && account.Type == models.Expense && account.ParentAccountId == expenses.AccountId {
groceries = (*accounts.Accounts)[i]
} else if account.Name == "Cable" && account.Type == models.Expense && account.ParentAccountId == expenses.AccountId {
cable = (*accounts.Accounts)[i]
}
}
if salary == nil {
t.Fatalf("Couldn't find 'Income/Salary' account")
}
if openingbalances == nil {
t.Fatalf("Couldn't find 'Equity/Opening Balances")
}
if creditcard == nil {
t.Fatalf("Couldn't find 'Liabilities/Credit Card' account")
}
if groceries == nil {
t.Fatalf("Couldn't find 'Expenses/Groceries' account")
}
if cable == nil {
t.Fatalf("Couldn't find 'Expenses/Cable' account")
}
accountBalanceHelper(t, d.clients[0], salary, "-998.34")
accountBalanceHelper(t, d.clients[0], creditcard, "-272.03")
accountBalanceHelper(t, d.clients[0], openingbalances, "-21014.33")
accountBalanceHelper(t, d.clients[0], groceries, "287.56") // 87.19 from preexisting transactions and 200.37 from Gnucash
accountBalanceHelper(t, d.clients[0], cable, "89.98")
var ge *models.Security
securities, err := getSecurities(d.clients[0])
if err != nil {
t.Fatalf("Error fetching securities: %s\n", err)
}
for i, security := range *securities.Securities {
if security.Symbol == "GE" {
ge = (*securities.Securities)[i]
}
}
if ge == nil {
t.Fatalf("Couldn't find GE security")
}
prices, err := getPrices(d.clients[0], ge.SecurityId)
if err != nil {
t.Fatalf("Error fetching prices: %s\n", err)
}
var p1787, p2894, p3170 bool
for _, price := range *prices.Prices {
if price.CurrencyId == d.securities[0].SecurityId && price.Value == "17.87" {
p1787 = true
} else if price.CurrencyId == d.securities[0].SecurityId && price.Value == "28.94" {
p2894 = true
} else if price.CurrencyId == d.securities[0].SecurityId && price.Value == "31.70" {
p3170 = true
}
}
if !p1787 || !p2894 || !p3170 {
t.Errorf("Error finding expected prices\n")
}
})
}

View File

@ -0,0 +1,229 @@
package integration_test
import (
"fmt"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"strconv"
"testing"
)
func importOFX(client *http.Client, accountid int64, filename string) error {
return uploadFile(client, filename, "/v1/accounts/"+strconv.FormatInt(accountid, 10)+"/imports/ofxfile")
}
func TestImportOFXChecking(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
// Ensure there's only one USD currency
oldDefault, err := getSecurity(d.clients[0], d.users[0].DefaultCurrency)
if err != nil {
t.Fatalf("Error fetching default security: %s\n", err)
}
d.users[0].DefaultCurrency = d.securities[0].SecurityId
if _, err := updateUser(d.clients[0], &d.users[0]); err != nil {
t.Fatalf("Error updating user: %s\n", err)
}
if err := deleteSecurity(d.clients[0], oldDefault); err != nil {
t.Fatalf("Error removing default security: %s\n", err)
}
// Import and ensure it didn't return a nasty error code
if err = importOFX(d.clients[0], d.accounts[1].AccountId, "testdata/checking_20171126.ofx"); err != nil {
t.Fatalf("Error importing OFX: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], &d.accounts[1], "2493.19")
if err = importOFX(d.clients[0], d.accounts[1].AccountId, "testdata/checking_20171129.ofx"); err != nil {
t.Fatalf("Error importing OFX: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], &d.accounts[1], "5336.27")
})
}
func TestImportOFXCreditCard(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
// Ensure there's only one USD currency
oldDefault, err := getSecurity(d.clients[0], d.users[0].DefaultCurrency)
if err != nil {
t.Fatalf("Error fetching default security: %s\n", err)
}
d.users[0].DefaultCurrency = d.securities[0].SecurityId
if _, err := updateUser(d.clients[0], &d.users[0]); err != nil {
t.Fatalf("Error updating user: %s\n", err)
}
if err := deleteSecurity(d.clients[0], oldDefault); err != nil {
t.Fatalf("Error removing default security: %s\n", err)
}
// Import and ensure it didn't return a nasty error code
if err = importOFX(d.clients[0], d.accounts[7].AccountId, "testdata/creditcard.ofx"); err != nil {
t.Fatalf("Error importing OFX: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], &d.accounts[7], "-4.49")
})
}
func findSecurity(client *http.Client, symbol string, tipe models.SecurityType) (*models.Security, error) {
securities, err := getSecurities(client)
if err != nil {
return nil, err
}
for _, security := range *securities.Securities {
if security.Symbol == symbol && security.Type == tipe {
return security, nil
}
}
return nil, fmt.Errorf("Unable to find security: \"%s\"", symbol)
}
func findAccount(client *http.Client, name string, tipe models.AccountType, securityid int64) (*models.Account, error) {
accounts, err := getAccounts(client)
if err != nil {
return nil, err
}
for _, account := range *accounts.Accounts {
if account.Name == name && account.Type == tipe && account.SecurityId == securityid {
return account, nil
}
}
return nil, fmt.Errorf("Unable to find account: \"%s\"", name)
}
func TestImportOFX401kMutualFunds(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
// Ensure there's only one USD currency
oldDefault, err := getSecurity(d.clients[0], d.users[0].DefaultCurrency)
if err != nil {
t.Fatalf("Error fetching default security: %s\n", err)
}
d.users[0].DefaultCurrency = d.securities[0].SecurityId
if _, err := updateUser(d.clients[0], &d.users[0]); err != nil {
t.Fatalf("Error updating user: %s\n", err)
}
if err := deleteSecurity(d.clients[0], oldDefault); err != nil {
t.Fatalf("Error removing default security: %s\n", err)
}
account := &models.Account{
SecurityId: d.securities[0].SecurityId,
UserId: d.users[0].UserId,
ParentAccountId: -1,
Type: models.Investment,
Name: "401k",
}
account, err = createAccount(d.clients[0], account)
if err != nil {
t.Fatalf("Error creating 401k account: %s\n", err)
}
// Import and ensure it didn't return a nasty error code
if err = importOFX(d.clients[0], account.AccountId, "testdata/401k_mutualfunds.ofx"); err != nil {
t.Fatalf("Error importing OFX: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], account, "-192.10")
// Make sure the security was created and that the trading account has
// the right value
security, err := findSecurity(d.clients[0], "VANGUARD TARGET 2045", models.Stock)
if err != nil {
t.Fatalf("Error finding VANGUARD TARGET 2045 security: %s\n", err)
}
tradingaccount, err := findAccount(d.clients[0], "VANGUARD TARGET 2045", models.Trading, security.SecurityId)
if err != nil {
t.Fatalf("Error finding VANGUARD TARGET 2045 trading account: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], tradingaccount, "-3.35400")
// Ensure actual holding account was created and in the correct place
investmentaccount, err := findAccount(d.clients[0], "VANGUARD TARGET 2045", models.Investment, security.SecurityId)
if err != nil {
t.Fatalf("Error finding VANGUARD TARGET 2045 investment account: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], investmentaccount, "3.35400")
if investmentaccount.ParentAccountId != account.AccountId {
t.Errorf("Expected imported security account to be child of investment account it's imported into\n")
}
})
}
func TestImportOFXBrokerage(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
// Ensure there's only one USD currency
oldDefault, err := getSecurity(d.clients[0], d.users[0].DefaultCurrency)
if err != nil {
t.Fatalf("Error fetching default security: %s\n", err)
}
d.users[0].DefaultCurrency = d.securities[0].SecurityId
if _, err := updateUser(d.clients[0], &d.users[0]); err != nil {
t.Fatalf("Error updating user: %s\n", err)
}
if err := deleteSecurity(d.clients[0], oldDefault); err != nil {
t.Fatalf("Error removing default security: %s\n", err)
}
// Create the brokerage account
account := &models.Account{
SecurityId: d.securities[0].SecurityId,
UserId: d.users[0].UserId,
ParentAccountId: -1,
Type: models.Investment,
Name: "Personal Brokerage",
}
account, err = createAccount(d.clients[0], account)
if err != nil {
t.Fatalf("Error creating 'Personal Brokerage' account: %s\n", err)
}
// Import and ensure it didn't return a nasty error code
if err = importOFX(d.clients[0], account.AccountId, "testdata/brokerage.ofx"); err != nil {
t.Fatalf("Error importing OFX: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], account, "387.48")
// Make sure the USD trading account was created and has the right
// value
usdtrading, err := findAccount(d.clients[0], "USD", models.Trading, d.users[0].DefaultCurrency)
if err != nil {
t.Fatalf("Error finding USD trading account: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], usdtrading, "619.96")
// Check investment/trading balances for all securities traded
checks := []struct {
Ticker string
Name string
Balance string
TradingBalance string
}{
{"VBMFX", "Vanguard Total Bond Market Index Fund Investor Shares", "37.70000", "-37.70000"},
{"921909768", "VANGUARD TOTAL INTL STOCK INDE", "5.00000", "-5.00000"},
{"ATO", "ATMOS ENERGY CORP", "0.08600", "-0.08600"},
{"VMFXX", "Vanguard Federal Money Market Fund", "-21.57000", "21.57000"},
}
for _, check := range checks {
security, err := findSecurity(d.clients[0], check.Ticker, models.Stock)
if err != nil {
t.Fatalf("Error finding security: %s\n", err)
}
account, err := findAccount(d.clients[0], check.Name, models.Investment, security.SecurityId)
if err != nil {
t.Fatalf("Error finding trading account: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], account, check.Balance)
tradingaccount, err := findAccount(d.clients[0], check.Name, models.Trading, security.SecurityId)
if err != nil {
t.Fatalf("Error finding trading account: %s\n", err)
}
accountBalanceHelper(t, d.clients[0], tradingaccount, check.TradingBalance)
}
// TODO check reinvestment/income to make sure they're registered as income?
})
}

View File

@ -0,0 +1,23 @@
package integration_test
import (
"fmt"
"strconv"
"testing"
)
func TestLuaPrices(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
security := d.securities[1]
currency := d.securities[0]
simpleLuaTest(t, d.clients[0], []LuaTest{
{"Security:ClosestPrice", fmt.Sprintf("secs = get_securities(); return secs[%d]:ClosestPrice(secs[%d], date.new('2016-11-19'))", security.SecurityId, currency.SecurityId), fmt.Sprintf("225.24 %s (%s)", currency.Symbol, security.Symbol)},
{"Security:ClosestPrice(2)", fmt.Sprintf("secs = get_securities(); return secs[%d]:ClosestPrice(secs[%d], date.new('2017-01-04'))", security.SecurityId, currency.SecurityId), fmt.Sprintf("226.58 %s (%s)", currency.Symbol, security.Symbol)},
{"PriceId", fmt.Sprintf("secs = get_securities(); return secs[%d]:ClosestPrice(secs[%d], date.new('2016-11-19')).PriceId", security.SecurityId, currency.SecurityId), strconv.FormatInt(d.prices[0].PriceId, 10)},
{"Security", fmt.Sprintf("secs = get_securities(); return secs[%d]:ClosestPrice(secs[%d], date.new('2016-11-19')).Security == secs[%d]", security.SecurityId, currency.SecurityId, security.SecurityId), "true"},
{"Currency", fmt.Sprintf("secs = get_securities(); return secs[%d]:ClosestPrice(secs[%d], date.new('2016-11-19')).Currency == secs[%d]", security.SecurityId, currency.SecurityId, currency.SecurityId), "true"},
{"Value", fmt.Sprintf("secs = get_securities(); return secs[%d]:ClosestPrice(secs[%d], date.new('2098-11-09')).Value", security.SecurityId, currency.SecurityId), "227.21"},
})
})
}

View File

@ -0,0 +1,219 @@
package integration_test
import (
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"strconv"
"testing"
"time"
)
func createPrice(client *http.Client, price *models.Price) (*models.Price, error) {
var p models.Price
err := create(client, price, &p, "/v1/securities/"+strconv.FormatInt(price.SecurityId, 10)+"/prices/")
return &p, err
}
func getPrice(client *http.Client, priceid, securityid int64) (*models.Price, error) {
var p models.Price
err := read(client, &p, "/v1/securities/"+strconv.FormatInt(securityid, 10)+"/prices/"+strconv.FormatInt(priceid, 10))
if err != nil {
return nil, err
}
return &p, nil
}
func getPrices(client *http.Client, securityid int64) (*models.PriceList, error) {
var pl models.PriceList
err := read(client, &pl, "/v1/securities/"+strconv.FormatInt(securityid, 10)+"/prices/")
if err != nil {
return nil, err
}
return &pl, nil
}
func updatePrice(client *http.Client, price *models.Price) (*models.Price, error) {
var p models.Price
err := update(client, price, &p, "/v1/securities/"+strconv.FormatInt(price.SecurityId, 10)+"/prices/"+strconv.FormatInt(price.PriceId, 10))
if err != nil {
return nil, err
}
return &p, nil
}
func deletePrice(client *http.Client, p *models.Price) error {
err := remove(client, "/v1/securities/"+strconv.FormatInt(p.SecurityId, 10)+"/prices/"+strconv.FormatInt(p.PriceId, 10))
if err != nil {
return err
}
return nil
}
func TestCreatePrice(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].prices); i++ {
orig := data[0].prices[i]
p := d.prices[i]
if p.PriceId == 0 {
t.Errorf("Unable to create price: %+v", p)
}
if p.SecurityId != d.securities[orig.SecurityId].SecurityId {
t.Errorf("SecurityId doesn't match")
}
if p.CurrencyId != d.securities[orig.CurrencyId].SecurityId {
t.Errorf("CurrencyId doesn't match")
}
if !p.Date.Equal(orig.Date) {
t.Errorf("Date doesn't match")
}
if p.Value != orig.Value {
t.Errorf("Value doesn't match")
}
if p.RemoteId != orig.RemoteId {
t.Errorf("RemoteId doesn't match")
}
}
})
}
func TestGetPrice(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].prices); i++ {
orig := data[0].prices[i]
curr := d.prices[i]
userid := data[0].securities[orig.SecurityId].UserId
p, err := getPrice(d.clients[userid], curr.PriceId, curr.SecurityId)
if err != nil {
t.Fatalf("Error fetching price: %s\n", err)
}
if p.SecurityId != d.securities[orig.SecurityId].SecurityId {
t.Errorf("SecurityId doesn't match")
}
if p.CurrencyId != d.securities[orig.CurrencyId].SecurityId {
t.Errorf("CurrencyId doesn't match")
}
if !p.Date.Equal(orig.Date) {
t.Errorf("Date doesn't match")
}
if p.Value != orig.Value {
t.Errorf("Value doesn't match")
}
if p.RemoteId != orig.RemoteId {
t.Errorf("RemoteId doesn't match")
}
}
})
}
func TestGetPrices(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for origsecurityid, security := range d.securities {
if data[0].securities[origsecurityid].UserId != 0 {
continue
}
pl, err := getPrices(d.clients[0], security.SecurityId)
if err != nil {
t.Fatalf("Error fetching prices: %s\n", err)
}
numprices := 0
foundIds := make(map[int64]bool)
for i := 0; i < len(data[0].prices); i++ {
orig := data[0].prices[i]
if orig.SecurityId != int64(origsecurityid) {
continue
}
numprices += 1
found := false
for _, p := range *pl.Prices {
if p.SecurityId == d.securities[orig.SecurityId].SecurityId && p.CurrencyId == d.securities[orig.CurrencyId].SecurityId && p.Date.Equal(orig.Date) && p.Value == orig.Value && p.RemoteId == orig.RemoteId {
if _, ok := foundIds[p.PriceId]; ok {
continue
}
foundIds[p.PriceId] = true
found = true
break
}
}
if !found {
t.Errorf("Unable to find matching price: %+v", orig)
}
}
if numprices != len(*pl.Prices) {
t.Fatalf("Expected %d prices, received %d", numprices, len(*pl.Prices))
}
}
})
}
func TestUpdatePrice(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].prices); i++ {
orig := data[0].prices[i]
curr := d.prices[i]
tmp := curr.SecurityId
curr.SecurityId = curr.CurrencyId
curr.CurrencyId = tmp
curr.Value = "5.55"
curr.Date = time.Date(2019, time.June, 5, 12, 5, 6, 7, time.UTC)
curr.RemoteId = "something"
userid := data[0].securities[orig.SecurityId].UserId
p, err := updatePrice(d.clients[userid], &curr)
if err != nil {
t.Fatalf("Error updating price: %s\n", err)
}
if p.SecurityId != curr.SecurityId {
t.Errorf("SecurityId doesn't match")
}
if p.CurrencyId != curr.CurrencyId {
t.Errorf("CurrencyId doesn't match")
}
if !p.Date.Equal(curr.Date) {
t.Errorf("Date doesn't match")
}
if p.Value != curr.Value {
t.Errorf("Value doesn't match")
}
if p.RemoteId != curr.RemoteId {
t.Errorf("RemoteId doesn't match")
}
}
})
}
func TestDeletePrice(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].prices); i++ {
orig := data[0].prices[i]
curr := d.prices[i]
userid := data[0].securities[orig.SecurityId].UserId
err := deletePrice(d.clients[userid], &curr)
if err != nil {
t.Fatalf("Error deleting price: %s\n", err)
}
_, err = getPrice(d.clients[userid], curr.PriceId, curr.SecurityId)
if err == nil {
t.Fatalf("Expected error fetching deleted price")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error fetching deleted price: %s", herr)
}
} else {
t.Fatalf("Unexpected error fetching deleted price")
}
}
})
}

View File

@ -0,0 +1,46 @@
package integration_test
import (
"fmt"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"testing"
)
type LuaTest struct {
Name string
Lua string
Expected string
}
func simpleLuaTest(t *testing.T, client *http.Client, tests []LuaTest) {
t.Helper()
for _, lt := range tests {
lua := fmt.Sprintf(`function test()
%s
end
function generate()
t = tabulation.new(0)
t:title(tostring(test()))
return t
end`, lt.Lua)
r := models.Report{
Name: lt.Name,
Lua: lua,
}
report, err := createReport(client, &r)
if err != nil {
t.Fatalf("Error creating report: %s", err)
}
tab, err := tabulateReport(client, report.ReportId)
if err != nil {
t.Fatalf("Error tabulating report: %s", err)
}
if tab.Title != lt.Expected {
t.Errorf("%s: Returned '%s', expected '%s'", lt.Name, tab.Title, lt.Expected)
}
}
}

View File

@ -0,0 +1,282 @@
package integration_test
import (
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"strconv"
"testing"
)
func createReport(client *http.Client, report *models.Report) (*models.Report, error) {
var r models.Report
err := create(client, report, &r, "/v1/reports/")
return &r, err
}
func getReport(client *http.Client, reportid int64) (*models.Report, error) {
var r models.Report
err := read(client, &r, "/v1/reports/"+strconv.FormatInt(reportid, 10))
if err != nil {
return nil, err
}
return &r, nil
}
func getReports(client *http.Client) (*models.ReportList, error) {
var rl models.ReportList
err := read(client, &rl, "/v1/reports/")
if err != nil {
return nil, err
}
return &rl, nil
}
func updateReport(client *http.Client, report *models.Report) (*models.Report, error) {
var r models.Report
err := update(client, report, &r, "/v1/reports/"+strconv.FormatInt(report.ReportId, 10))
if err != nil {
return nil, err
}
return &r, nil
}
func deleteReport(client *http.Client, r *models.Report) error {
err := remove(client, "/v1/reports/"+strconv.FormatInt(r.ReportId, 10))
if err != nil {
return err
}
return nil
}
func tabulateReport(client *http.Client, reportid int64) (*models.Tabulation, error) {
var t models.Tabulation
err := read(client, &t, "/v1/reports/"+strconv.FormatInt(reportid, 10)+"/tabulations")
if err != nil {
return nil, err
}
return &t, nil
}
func TestCreateReport(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].reports); i++ {
orig := data[0].reports[i]
r := d.reports[i]
if r.ReportId == 0 {
t.Errorf("Unable to create report: %+v", r)
}
if r.Name != orig.Name {
t.Errorf("Name doesn't match")
}
if r.Lua != orig.Lua {
t.Errorf("Lua doesn't match")
}
r.Lua = string(make([]byte, models.LuaMaxLength+1))
_, err := createReport(d.clients[orig.UserId], &r)
if err == nil {
t.Fatalf("Expected error creating report with too-long Lua")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error creating report with too-long Lua: %s", herr)
}
} else {
t.Fatalf("Unexpected error creating report with too-long Lua")
}
}
})
}
func TestGetReport(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].reports); i++ {
orig := data[0].reports[i]
curr := d.reports[i]
r, err := getReport(d.clients[orig.UserId], curr.ReportId)
if err != nil {
t.Fatalf("Error fetching reports: %s\n", err)
}
if r.Name != orig.Name {
t.Errorf("Name doesn't match")
}
if r.Lua != orig.Lua {
t.Errorf("Lua doesn't match")
}
}
})
}
func TestGetReports(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
rl, err := getReports(d.clients[0])
if err != nil {
t.Fatalf("Error fetching reports: %s\n", err)
}
numreports := 0
foundIds := make(map[int64]bool)
for i := 0; i < len(data[0].reports); i++ {
orig := data[0].reports[i]
curr := d.reports[i]
if curr.UserId != d.users[0].UserId {
continue
}
numreports += 1
found := false
for _, r := range *rl.Reports {
if orig.Name == r.Name && orig.Lua == r.Lua {
if _, ok := foundIds[r.ReportId]; ok {
continue
}
foundIds[r.ReportId] = true
found = true
break
}
}
if !found {
t.Errorf("Unable to find matching report: %+v", orig)
}
}
if numreports != len(*rl.Reports) {
t.Fatalf("Expected %d reports, received %d", numreports, len(*rl.Reports))
}
})
}
func TestUpdateReport(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].reports); i++ {
orig := data[0].reports[i]
curr := d.reports[i]
curr.Name = "blah"
curr.Lua = "empty"
r, err := updateReport(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error updating report: %s\n", err)
}
if r.ReportId != curr.ReportId {
t.Errorf("ReportId doesn't match")
}
if r.Name != curr.Name {
t.Errorf("Name doesn't match")
}
if r.Lua != curr.Lua {
t.Errorf("Lua doesn't match")
}
r.Lua = string(make([]byte, models.LuaMaxLength+1))
_, err = updateReport(d.clients[orig.UserId], r)
if err == nil {
t.Fatalf("Expected error updating report with too-long Lua")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error updating report with too-long Lua: %s", herr)
}
} else {
t.Fatalf("Unexpected error updating report with too-long Lua")
}
}
})
}
func TestDeleteReport(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].reports); i++ {
orig := data[0].reports[i]
curr := d.reports[i]
err := deleteReport(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error deleting report: %s\n", err)
}
_, err = getReport(d.clients[orig.UserId], curr.ReportId)
if err == nil {
t.Fatalf("Expected error fetching deleted report")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error fetching deleted report: %s", herr)
}
} else {
t.Fatalf("Unexpected error fetching deleted report")
}
}
})
}
func seriesEqualityHelper(t *testing.T, orig, curr map[string]*models.Series, name string) {
if orig == nil || curr == nil {
if orig != nil {
t.Fatalf("`%s` series unexpectedly nil", name)
}
if curr != nil {
t.Fatalf("`%s` series unexpectedly non-nil", name)
}
return
}
if len(orig) != len(curr) {
t.Errorf("Series in question: %v\n", curr)
t.Fatalf("Series' don't contain the same number of sub-series (found %d, expected %d)", len(curr), len(orig))
}
for k, os := range orig {
cs := curr[k]
if len(os.Values) != len(cs.Values) {
t.Fatalf("`%s` series doesn't contain the same number of Values (found %d, expected %d)", k, len(cs.Values), len(os.Values))
}
for i, v := range os.Values {
if v != cs.Values[i] {
t.Errorf("Series doesn't contain the same values (found %f, expected %f)", cs.Values[i], v)
}
}
seriesEqualityHelper(t, os.Series, cs.Series, k)
}
}
func tabulationEqualityHelper(t *testing.T, orig, curr *models.Tabulation) {
if orig.Title != curr.Title {
t.Errorf("Tabulation Title doesn't match")
}
if orig.Subtitle != curr.Subtitle {
t.Errorf("Tabulation Subtitle doesn't match")
}
if orig.Units != curr.Units {
t.Errorf("Tabulation Units doesn't match")
}
if len(orig.Labels) != len(curr.Labels) {
t.Fatalf("Tabulation doesn't contain the same number of labels")
}
for i, label := range orig.Labels {
if label != curr.Labels[i] {
t.Errorf("Label %d doesn't match", i)
}
}
seriesEqualityHelper(t, orig.Series, curr.Series, "top-level")
}
func TestTabulateReport(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].tabulations); i++ {
orig := data[0].tabulations[i]
origReport := data[0].reports[orig.ReportId]
report := d.reports[orig.ReportId]
rt2, err := tabulateReport(d.clients[origReport.UserId], report.ReportId)
if err != nil {
t.Fatalf("Unexpected error tabulating report")
}
tabulationEqualityHelper(t, &orig, rt2)
}
})
}

View File

@ -0,0 +1,87 @@
package integration_test
import (
"fmt"
"sort"
"strconv"
"testing"
)
// Int64Slice attaches the methods of int64 to []int64, sorting in increasing order.
type Int64Slice []int64
func (p Int64Slice) Len() int { return len(p) }
func (p Int64Slice) Less(i, j int) bool { return p[i] < p[j] }
func (p Int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// Sort is a convenience method.
func (p Int64Slice) Sort() { sort.Sort(p) }
func TestLuaSecurities(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
defaultSecurity, err := getSecurity(d.clients[0], d.users[0].DefaultCurrency)
if err != nil {
t.Fatalf("Error getting default security: %s", err)
}
securities, err := getSecurities(d.clients[0])
if err != nil {
t.Fatalf("Error getting securities: %s", err)
}
securityids := make(Int64Slice, len(*securities.Securities))
for i, s := range *securities.Securities {
securityids[i] = s.SecurityId
}
securityids.Sort()
equalityString := ""
for i := range securityids {
for j := range securityids {
if i == j {
equalityString += "true"
} else {
equalityString += "false"
}
}
}
simpleLuaTest(t, d.clients[0], []LuaTest{
{"__tostring", `return get_default_currency()`, fmt.Sprintf("%s - %s (%s)", defaultSecurity.Name, defaultSecurity.Description, defaultSecurity.Symbol)},
{"SecurityId", `return get_default_currency().SecurityId`, strconv.FormatInt(defaultSecurity.SecurityId, 10)},
{"Name", `return get_default_currency().Name`, defaultSecurity.Name},
{"Description", `return get_default_currency().Description`, defaultSecurity.Description},
{"Symbol", `return get_default_currency().Symbol`, defaultSecurity.Symbol},
{"Precision", `return get_default_currency().Precision`, strconv.FormatInt(int64(defaultSecurity.Precision), 10)},
{"Type", `return get_default_currency().Type`, strconv.FormatInt(int64(defaultSecurity.Type), 10)},
{"AlternateId", `return get_default_currency().AlternateId`, defaultSecurity.AlternateId},
{"__eq", `
securities = get_securities()
sorted = {}
for id in pairs(securities) do
table.insert(sorted, id)
end
str = ""
table.sort(sorted)
for i,idi in ipairs(sorted) do
for j,idj in ipairs(sorted) do
if securities[idi] == securities[idj] then
str = str .. "true"
else
str = str .. "false"
end
end
end
return str`, equalityString},
{"get_securities()", `
sorted = {}
for id in pairs(get_securities()) do
table.insert(sorted, id)
end
table.sort(sorted)
str = "["
for i,id in ipairs(sorted) do
str = str .. id .. " "
end
return string.sub(str, 1, -2) .. "]"`, fmt.Sprint(securityids)},
})
})
}

View File

@ -0,0 +1,267 @@
package integration_test
import (
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"strconv"
"testing"
)
func createSecurity(client *http.Client, security *models.Security) (*models.Security, error) {
var s models.Security
err := create(client, security, &s, "/v1/securities/")
return &s, err
}
func getSecurity(client *http.Client, securityid int64) (*models.Security, error) {
var s models.Security
err := read(client, &s, "/v1/securities/"+strconv.FormatInt(securityid, 10))
if err != nil {
return nil, err
}
return &s, nil
}
func getSecurities(client *http.Client) (*models.SecurityList, error) {
var sl models.SecurityList
err := read(client, &sl, "/v1/securities/")
if err != nil {
return nil, err
}
return &sl, nil
}
func updateSecurity(client *http.Client, security *models.Security) (*models.Security, error) {
var s models.Security
err := update(client, security, &s, "/v1/securities/"+strconv.FormatInt(security.SecurityId, 10))
if err != nil {
return nil, err
}
return &s, nil
}
func deleteSecurity(client *http.Client, s *models.Security) error {
err := remove(client, "/v1/securities/"+strconv.FormatInt(s.SecurityId, 10))
if err != nil {
return err
}
return nil
}
func TestCreateSecurity(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].securities); i++ {
orig := data[0].securities[i]
s := d.securities[i]
if s.SecurityId == 0 {
t.Errorf("Unable to create security: %+v", s)
}
if s.Name != orig.Name {
t.Errorf("Name doesn't match")
}
if s.Description != orig.Description {
t.Errorf("Description doesn't match")
}
if s.Symbol != orig.Symbol {
t.Errorf("Symbol doesn't match")
}
if s.Precision != orig.Precision {
t.Errorf("Precision doesn't match")
}
if s.Type != orig.Type {
t.Errorf("Type doesn't match")
}
if s.AlternateId != orig.AlternateId {
t.Errorf("AlternateId doesn't match")
}
}
})
}
func TestGetSecurity(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].securities); i++ {
orig := data[0].securities[i]
curr := d.securities[i]
s, err := getSecurity(d.clients[orig.UserId], curr.SecurityId)
if err != nil {
t.Fatalf("Error fetching security: %s\n", err)
}
if s.SecurityId != curr.SecurityId {
t.Errorf("SecurityId doesn't match %+v %+v", s, curr)
}
if s.Name != orig.Name {
t.Errorf("Name doesn't match")
}
if s.Description != orig.Description {
t.Errorf("Description doesn't match")
}
if s.Symbol != orig.Symbol {
t.Errorf("Symbol doesn't match")
}
if s.Precision != orig.Precision {
t.Errorf("Precision doesn't match")
}
if s.Type != orig.Type {
t.Errorf("Type doesn't match")
}
if s.AlternateId != orig.AlternateId {
t.Errorf("AlternateId doesn't match")
}
}
})
}
func TestGetSecurities(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
sl, err := getSecurities(d.clients[0])
if err != nil {
t.Fatalf("Error fetching securities: %s\n", err)
}
numsecurities := 0
foundIds := make(map[int64]bool)
for i := 0; i < len(data[0].securities); i++ {
orig := data[0].securities[i]
curr := d.securities[i]
if curr.UserId != d.users[0].UserId {
continue
}
numsecurities += 1
found := false
for _, s := range *sl.Securities {
if orig.Name == s.Name && orig.Description == s.Description && orig.Symbol == orig.Symbol && orig.Precision == s.Precision && orig.Type == s.Type && orig.AlternateId == s.AlternateId {
if _, ok := foundIds[s.SecurityId]; ok {
continue
}
foundIds[s.SecurityId] = true
found = true
break
}
}
if !found {
t.Errorf("Unable to find matching security: %+v", curr)
}
}
if numsecurities+1 == len(*sl.Securities) {
for _, s := range *sl.Securities {
if _, ok := foundIds[s.SecurityId]; !ok {
if s.SecurityId == d.users[0].DefaultCurrency {
t.Fatalf("Extra security wasn't default currency, seems like an extra security was created")
}
break
}
}
} else if numsecurities != len(*sl.Securities) {
t.Fatalf("Expected %d securities, received %d", numsecurities, len(*sl.Securities))
}
})
}
func TestUpdateSecurity(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].securities); i++ {
orig := data[0].securities[i]
curr := d.securities[i]
curr.Name = "EUR"
curr.Description = "Euro"
curr.Symbol = "€"
curr.AlternateId = "978"
s, err := updateSecurity(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error updating security: %s\n", err)
}
if s.SecurityId != curr.SecurityId {
t.Errorf("SecurityId doesn't match")
}
if s.Name != curr.Name {
t.Errorf("Name doesn't match")
}
if s.Description != curr.Description {
t.Errorf("Description doesn't match")
}
if s.Symbol != curr.Symbol {
t.Errorf("Symbol doesn't match")
}
if s.Precision != curr.Precision {
t.Errorf("Precision doesn't match")
}
if s.Type != curr.Type {
t.Errorf("Type doesn't match")
}
if s.AlternateId != curr.AlternateId {
t.Errorf("AlternateId doesn't match")
}
}
})
}
func TestDeleteSecurity(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
Outer:
for i := 0; i < len(data[0].securities); i++ {
orig := data[0].securities[i]
curr := d.securities[i]
for _, a := range d.accounts {
if a.SecurityId == curr.SecurityId {
continue Outer
}
}
err := deleteSecurity(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error deleting security: %s\n", err)
}
_, err = getSecurity(d.clients[orig.UserId], curr.SecurityId)
if err == nil {
t.Fatalf("Expected error fetching deleted security")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error fetching deleted security: %s", herr)
}
} else {
t.Fatalf("Unexpected error fetching deleted security")
}
}
})
}
func TestDontDeleteSecurity(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
Outer:
for i := 0; i < len(data[0].securities); i++ {
orig := data[0].securities[i]
curr := d.securities[i]
for _, a := range d.accounts {
if a.SecurityId != curr.SecurityId {
continue Outer
}
}
err := deleteSecurity(d.clients[orig.UserId], &curr)
if err == nil {
t.Fatalf("Expected error deleting in-use security")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 7 { // In Use Error
t.Fatalf("Unexpected API error deleting in-use security: %s", herr)
}
} else {
t.Fatalf("Unexpected error deleting in-use security")
}
}
})
}

View File

@ -0,0 +1,123 @@
package integration_test
import (
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"io/ioutil"
"testing"
)
func TestSecurityTemplates(t *testing.T) {
var sl models.SecurityList
response, err := server.Client().Get(server.URL + "/v1/securitytemplates/?search=USD&type=currency")
if err != nil {
t.Fatal(err)
}
if response.StatusCode != 200 {
t.Fatalf("Unexpected HTTP status code: %d\n", response.StatusCode)
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
t.Fatal(err)
}
err = (&sl).Read(string(body))
if err != nil {
t.Fatal(err)
}
num_usd := 0
if sl.Securities != nil {
for _, s := range *sl.Securities {
if s.Type != models.Currency {
t.Fatalf("Requested Currency-only security templates, received a non-Currency template for %s", s.Name)
}
if s.Name == "USD" && s.AlternateId == "840" {
num_usd++
}
}
}
if num_usd != 1 {
t.Fatalf("Expected one USD security template, found %d\n", num_usd)
}
}
func TestSecurityTemplateLimit(t *testing.T) {
var sl models.SecurityList
response, err := server.Client().Get(server.URL + "/v1/securitytemplates/?search=e&limit=5")
if err != nil {
t.Fatal(err)
}
if response.StatusCode != 200 {
t.Fatalf("Unexpected HTTP status code: %d\n", response.StatusCode)
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
t.Fatal(err)
}
err = (&sl).Read(string(body))
if err != nil {
t.Fatal(err)
}
if sl.Securities == nil {
t.Fatalf("Securities was unexpectedly nil\n")
}
if len(*sl.Securities) > 5 {
t.Fatalf("Requested only 5 securities, received %d\n", len(*sl.Securities))
}
}
func TestSecurityTemplateInvalidType(t *testing.T) {
var e handlers.Error
response, err := server.Client().Get(server.URL + "/v1/securitytemplates/?search=e&type=blah")
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
t.Fatal(err)
}
err = (&e).Read(string(body))
if err != nil {
t.Fatal(err)
}
if e.ErrorId != 3 {
t.Fatal("Expected ErrorId 3, Invalid Request")
}
}
func TestSecurityTemplateInvalidLimit(t *testing.T) {
var e handlers.Error
response, err := server.Client().Get(server.URL + "/v1/securitytemplates/?search=e&type=Currency&limit=foo")
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
t.Fatal(err)
}
err = (&e).Read(string(body))
if err != nil {
t.Fatal(err)
}
if e.ErrorId != 3 {
t.Fatal("Expected ErrorId 3, Invalid Request")
}
}

View File

@ -0,0 +1,121 @@
package integration_test
import (
"fmt"
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"net/http/cookiejar"
"net/url"
"testing"
)
func newSession(user *User) (*http.Client, error) {
var u User
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: nil})
if err != nil {
return nil, err
}
var client http.Client
client = *server.Client()
client.Jar = jar
create(&client, user, &u, "/v1/sessions/")
return &client, nil
}
func getSession(client *http.Client) (*models.Session, error) {
var s models.Session
err := read(client, &s, "/v1/sessions/")
return &s, err
}
func deleteSession(client *http.Client) error {
return remove(client, "/v1/sessions/")
}
func sessionExistsOrError(c *http.Client) error {
url, err := url.Parse(server.URL)
if err != nil {
return err
}
cookies := c.Jar.Cookies(url)
var found_session bool = false
for _, cookie := range cookies {
if cookie.Name == "moneygo-session" {
found_session = true
}
}
if found_session {
return nil
}
return fmt.Errorf("Didn't find 'moneygo-session' cookie in CookieJar")
}
func TestCreateSession(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
if err := sessionExistsOrError(d.clients[0]); err != nil {
t.Fatal(err)
}
})
}
func TestGetSession(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
session, err := getSession(d.clients[0])
if err != nil {
t.Fatal(err)
}
if len(session.SessionSecret) != 0 {
t.Error("Session.SessionSecret should not be passed back in JSON")
}
if session.UserId != d.users[0].UserId {
t.Errorf("session's UserId (%d) should equal user's UserID (%d)", session.UserId, d.users[0].UserId)
}
if session.SessionId == 0 {
t.Error("session's SessionId should not be 0")
}
})
}
func TestDeleteSession(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
err := deleteSession(d.clients[0])
if err != nil {
t.Fatalf("Unexpected error removing session: %s\n", err)
}
err = deleteSession(d.clients[0])
if err != nil {
t.Fatalf("Unexpected error attempting to delete nonexistent session: %s\n", err)
}
_, err = getSession(d.clients[0])
if err == nil {
t.Fatalf("Expected error fetching deleted session")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 1 { // Not Signed in
t.Fatalf("Unexpected API error fetching deleted session: %s", herr)
}
} else {
t.Fatalf("Unexpected error fetching deleted session")
}
// Login again so we don't screw up the TestData teardown code
userWithPassword := d.users[0]
userWithPassword.Password = data[0].users[0].Password
client, err := newSession(&userWithPassword)
if err != nil {
t.Fatalf("Unexpected error re-creating session: %s\n", err)
}
d.clients[0] = client
})
}

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><?OFX OFXHEADER="200" VERSION="203" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?><OFX><SIGNONMSGSRSV1> <SONRS> <STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY><MESSAGE>SUCCESS</MESSAGE></STATUS><DTSERVER>20171128203521.622[-5:EST]</DTSERVER><LANGUAGE>ENG</LANGUAGE><FI><ORG>ofx.bank.com</ORG><FID>9199</FID></FI></SONRS></SIGNONMSGSRSV1><INVSTMTMSGSRSV1> <INVSTMTTRNRS><TRNUID>d87db96a-c872-7f73-7637-7e9e2816c25a</TRNUID> <STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY><MESSAGE>SUCCESS</MESSAGE></STATUS><INVSTMTRS><DTASOF>20171128193521.926[-5:EST]</DTASOF><CURDEF>USD</CURDEF><INVACCTFROM><BROKERID>ofx.bank.com</BROKERID><ACCTID>12321</ACCTID></INVACCTFROM> <INVTRANLIST><DTSTART>20170829213521.814[-4:EDT]</DTSTART><DTEND>20171127203521.814[-5:EST]</DTEND><BUYMF><INVBUY><INVTRAN><FITID>20170901OAEL011</FITID><DTTRADE>20170901070000.000[-4:EDT]</DTTRADE><MEMO>CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 09/01/2017</MEMO></INVTRAN><SECID><UNIQUEID>OAEL</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>1.756</UNITS><UNITPRICE>56.97</UNITPRICE><TOTAL>100.05</TOTAL><SUBACCTSEC>OTHER</SUBACCTSEC><SUBACCTFUND>OTHER</SUBACCTFUND></INVBUY><BUYTYPE>BUY</BUYTYPE> </BUYMF><BUYMF><INVBUY><INVTRAN><FITID>20170915OAEL011</FITID><DTTRADE>20170915070000.000[-4:EDT]</DTTRADE><MEMO>CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 09/15/2017</MEMO></INVTRAN><SECID><UNIQUEID>OAEL</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>1.737</UNITS><UNITPRICE>57.59</UNITPRICE><TOTAL>100.05</TOTAL><SUBACCTSEC>OTHER</SUBACCTSEC><SUBACCTFUND>OTHER</SUBACCTFUND></INVBUY><BUYTYPE>BUY</BUYTYPE> </BUYMF><SELLMF><INVSELL><INVTRAN><FITID>20170901OAEL131</FITID><DTTRADE>20170901070000.000[-4:EDT]</DTTRADE><MEMO>FEES;VANGUARD TARGET 2045 OAEL;as of 09/01/2017</MEMO></INVTRAN><SECID><UNIQUEID>OAEL</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>0.07</UNITS><UNITPRICE>56.97</UNITPRICE><TOTAL>4.0</TOTAL><SUBACCTSEC>OTHER</SUBACCTSEC><SUBACCTFUND>OTHER</SUBACCTFUND></INVSELL><SELLTYPE>SELL</SELLTYPE> </SELLMF><SELLMF><INVSELL><INVTRAN><FITID>20171002OAEL131</FITID><DTTRADE>20171002070000.000[-4:EDT]</DTTRADE><MEMO>FEES;VANGUARD TARGET 2045 OAEL;as of 10/02/2017</MEMO></INVTRAN><SECID><UNIQUEID>OAEL</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>0.069</UNITS><UNITPRICE>58.1</UNITPRICE><TOTAL>4.0</TOTAL><SUBACCTSEC>OTHER</SUBACCTSEC><SUBACCTFUND>OTHER</SUBACCTFUND></INVSELL><SELLTYPE>SELL</SELLTYPE> </SELLMF></INVTRANLIST> <INVPOSLIST><POSMF><INVPOS><SECID><UNIQUEID>OAEL</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><HELDINACCT>OTHER</HELDINACCT><POSTYPE>LONG</POSTYPE><UNITS>2792.373</UNITS><UNITPRICE>59.64</UNITPRICE><MKTVAL>200.03</MKTVAL> <DTPRICEASOF>20171127160000.000[-5:EST]</DTPRICEASOF> <MEMO>Market close as of 11/27/2017;VANGUARD TARGET 2045</MEMO></INVPOS></POSMF></INVPOSLIST> <INVBAL><AVAILCASH>0</AVAILCASH><MARGINBALANCE>0</MARGINBALANCE><SHORTBALANCE>0</SHORTBALANCE><BALLIST><BAL><NAME>MarketValue</NAME><DESC>MarketValue</DESC><BALTYPE>DOLLAR</BALTYPE><VALUE>200.03</VALUE><DTASOF>20171128193521.926[-5:EST]</DTASOF></BAL><BAL><NAME>VestedValue</NAME><DESC>VestedValue</DESC><BALTYPE>DOLLAR</BALTYPE><VALUE>200.03</VALUE><DTASOF>20171128193521.926[-5:EST]</DTASOF></BAL><BAL><NAME>TotalAssetsValue</NAME><DESC>TotalAssetsValue</DESC><BALTYPE>DOLLAR</BALTYPE><VALUE>200.03</VALUE><DTASOF>20171128193521.926[-5:EST]</DTASOF></BAL></BALLIST></INVBAL><INV401K><EMPLOYERNAME>QC 401(K) PLAN</EMPLOYERNAME></INV401K><INV401KBAL><TOTAL>200.03</TOTAL><BALLIST><BAL><NAME>MarketValue</NAME><DESC>MarketValue</DESC><BALTYPE>DOLLAR</BALTYPE><VALUE>200.03</VALUE><DTASOF>20171128193521.926[-5:EST]</DTASOF></BAL><BAL><NAME>VestedValue</NAME><DESC>VestedValue</DESC><BALTYPE>DOLLAR</BALTYPE><VALUE>200.03</VALUE><DTASOF>20171128193521.926[-5:EST]</DTASOF></BAL><BAL><NAME>TotalAssetsValue</NAME><DESC>TotalAssetsValue</DESC><BALTYPE>DOLLAR</BALTYPE><VALUE>200.03</VALUE><DTASOF>20171128193521.926[-5:EST]</DTASOF></BAL></BALLIST></INV401KBAL></INVSTMTRS></INVSTMTTRNRS></INVSTMTMSGSRSV1> <SECLISTMSGSRSV1><SECLIST><MFINFO><SECINFO><SECID><UNIQUEID>OAEL</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><SECNAME>VANGUARD TARGET 2045</SECNAME><FIID>OAEL</FIID><UNITPRICE>59.64</UNITPRICE><DTASOF>20171127160000.000[-5:EST]</DTASOF><MEMO>Market close as of 11/27/2017;VANGUARD TARGET 2045</MEMO></SECINFO><MFTYPE>OTHER</MFTYPE></MFINFO></SECLIST></SECLISTMSGSRSV1></OFX>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?><?OFX OFXHEADER="200" VERSION="202" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?><OFX><SIGNONMSGSRSV1><SONRS><STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY><MESSAGE>Successful Sign On</MESSAGE></STATUS><DTSERVER>20171130013742</DTSERVER><LANGUAGE>ENG</LANGUAGE><DTPROFUP>20160713012000</DTPROFUP><FI><ORG>Somewhere</ORG><FID>92772</FID></FI><SESSCOOKIE>01927017240917209172407124984652986</SESSCOOKIE></SONRS></SIGNONMSGSRSV1><INVSTMTMSGSRSV1><INVSTMTTRNRS><TRNUID>a59df4c3-9408-00bb-fd6d-0a06695885b1</TRNUID><STATUS><CODE>0</CODE><SEVERITY>INFO</SEVERITY></STATUS><INVSTMTRS><DTASOF>20171129160000.000[-5:EST]</DTASOF><CURDEF>USD</CURDEF><INVACCTFROM><BROKERID>investing.example.com</BROKERID><ACCTID>73728292</ACCTID></INVACCTFROM><INVTRANLIST><DTSTART>20160529160000.000[-5:EST]160000.000[-5:EST]</DTSTART><DTEND>20171130013742.000[-5:EST]</DTEND>
<BUYMF><INVBUY><INVTRAN><FITID>993971056</FITID><DTTRADE>20170315160000.000[-5:EST]</DTTRADE><DTSETTLE>20170316160000.000[-5:EST]</DTSETTLE><MEMO>BUY</MEMO></INVTRAN><SECID><UNIQUEID>921937108</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>37.700</UNITS><UNITPRICE>10.61</UNITPRICE><TOTAL>-400.0</TOTAL><SUBACCTSEC>CASH</SUBACCTSEC><SUBACCTFUND>CASH</SUBACCTFUND></INVBUY><BUYTYPE>BUY</BUYTYPE></BUYMF>
<BUYSTOCK><INVBUY><INVTRAN><FITID>206046941</FITID><DTTRADE>20160620160000.000[-5:EST]</DTTRADE><DTSETTLE>20160623160000.000[-5:EST]</DTSETTLE><MEMO>BUY</MEMO></INVTRAN><SECID><UNIQUEID>921909768</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>15.0</UNITS><UNITPRICE>45.17987</UNITPRICE><TOTAL>-677.70</TOTAL><SUBACCTSEC>CASH</SUBACCTSEC><SUBACCTFUND>CASH</SUBACCTFUND></INVBUY><BUYTYPE>BUY</BUYTYPE></BUYSTOCK>
<INCOME><INVTRAN><FITID>63590590</FITID><DTTRADE>20160729160000.000[-5:EST]</DTTRADE><DTSETTLE>20160729160000.000[-5:EST]</DTSETTLE><MEMO>DIVIDEND PAYMENTDIVIDEND PAYMENT</MEMO></INVTRAN><SECID><UNIQUEID>78462F103</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><INCOMETYPE>DIV</INCOMETYPE><TOTAL>1.08</TOTAL><SUBACCTSEC>CASH</SUBACCTSEC><SUBACCTFUND>CASH</SUBACCTFUND></INCOME>
<REINVEST><INVTRAN><FITID>769223517</FITID><DTTRADE>20160606160000.000[-5:EST]</DTTRADE><DTSETTLE>20160606160000.000[-5:EST]</DTSETTLE><MEMO>DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT</MEMO></INVTRAN><SECID><UNIQUEID>049560105</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><INCOMETYPE>DIV</INCOMETYPE><TOTAL>-6.43</TOTAL><SUBACCTSEC>CASH</SUBACCTSEC><UNITS>0.086</UNITS><UNITPRICE>74.9777</UNITPRICE></REINVEST>
<SELLMF><INVSELL><INVTRAN><FITID>619689018</FITID><DTTRADE>20160627160000.000[-5:EST]</DTTRADE><DTSETTLE>20160627160000.000[-5:EST]</DTSETTLE><MEMO>MONEY FUND REDEMPTION</MEMO></INVTRAN><SECID><UNIQUEID>922906300</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>-21.57</UNITS><UNITPRICE>1.0</UNITPRICE><TOTAL>21.57</TOTAL><SUBACCTSEC>CASH</SUBACCTSEC><SUBACCTFUND>CASH</SUBACCTFUND></INVSELL><SELLTYPE>SELL</SELLTYPE></SELLMF>
<SELLSTOCK><INVSELL><INVTRAN><FITID>328444499</FITID><DTTRADE>20160708160000.000[-5:EST]</DTTRADE><DTSETTLE>20160713160000.000[-5:EST]</DTSETTLE><MEMO>SELL</MEMO></INVTRAN><SECID><UNIQUEID>921909768</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><UNITS>-10.0</UNITS><UNITPRICE>44.26013</UNITPRICE><FEES>0.07</FEES><TOTAL>442.53</TOTAL><SUBACCTSEC>CASH</SUBACCTSEC><SUBACCTFUND>CASH</SUBACCTFUND></INVSELL><SELLTYPE>SELL</SELLTYPE></SELLSTOCK>
<INVBANKTRAN><STMTTRN><TRNTYPE>OTHER</TRNTYPE><DTPOSTED>20160607160000.000[-5:EST]</DTPOSTED><TRNAMT>1000.0</TRNAMT><FITID>230048208</FITID></STMTTRN><SUBACCTFUND>CASH</SUBACCTFUND></INVBANKTRAN>
</INVTRANLIST>
<INVPOSLIST><POSSTOCK><INVPOS><SECID><UNIQUEID>049560105</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><HELDINACCT>CASH</HELDINACCT><POSTYPE>LONG</POSTYPE><UNITS>6.086</UNITS><UNITPRICE>90.51</UNITPRICE><MKTVAL>550.843</MKTVAL><DTPRICEASOF>20171128160000.000[-5:EST]</DTPRICEASOF><MEMO>Price as of date based on closing price</MEMO></INVPOS><REINVDIV>Y</REINVDIV></POSSTOCK><POSMF><INVPOS><SECID><UNIQUEID>921937108</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><HELDINACCT>CASH</HELDINACCT><POSTYPE>LONG</POSTYPE><UNITS>37.77</UNITS><UNITPRICE>10.75</UNITPRICE><MKTVAL>406.02</MKTVAL><DTPRICEASOF>20171128160000.000[-5:EST]</DTPRICEASOF><MEMO>Price as of date based on closing price</MEMO></INVPOS><REINVDIV>Y</REINVDIV><REINVCG>Y</REINVCG></POSMF><POSMF><INVPOS><SECID><UNIQUEID>922906300</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><HELDINACCT>CASH</HELDINACCT><POSTYPE>LONG</POSTYPE><UNITS>24.87</UNITS><UNITPRICE>1.0</UNITPRICE><MKTVAL>24.87</MKTVAL><DTPRICEASOF>20171128160000.000[-5:EST]</DTPRICEASOF><MEMO>Price as of date based on closing price</MEMO></INVPOS><REINVDIV>N</REINVDIV><REINVCG>N</REINVCG></POSMF></INVPOSLIST>
<INVBAL><AVAILCASH>387.48</AVAILCASH><MARGINBALANCE>0.0</MARGINBALANCE><SHORTBALANCE>0.0</SHORTBALANCE></INVBAL></INVSTMTRS></INVSTMTTRNRS></INVSTMTMSGSRSV1><SECLISTMSGSRSV1><SECLIST><STOCKINFO><SECINFO><SECID><UNIQUEID>049560105</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><SECNAME>ATMOS ENERGY CORP</SECNAME><TICKER>ATO</TICKER><UNITPRICE>90.51</UNITPRICE><MEMO>Price as of date based on closing price</MEMO></SECINFO><STOCKTYPE>COMMON</STOCKTYPE><YIELD>2.1491</YIELD></STOCKINFO><STOCKINFO><SECINFO><SECID><UNIQUEID>921909768</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><SECNAME>VANGUARD TOTAL INTL STOCK INDE</SECNAME><TICKER>921909768</TICKER><MEMO>BUY</MEMO></SECINFO></STOCKINFO><STOCKINFO><SECINFO><SECID><UNIQUEID>78462F103</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><SECNAME>20SPDR SP 500 ETF</SECNAME><TICKER>78462F103</TICKER><MEMO>SELL</MEMO></SECINFO></STOCKINFO><MFINFO><SECINFO><SECID><UNIQUEID>921937108</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><SECNAME>Vanguard Total Bond Market Index Fund Investor Shares</SECNAME><TICKER>VBMFX</TICKER><UNITPRICE>10.75</UNITPRICE><MEMO>Price as of date based on closing price</MEMO></SECINFO><MFTYPE>OPENEND</MFTYPE></MFINFO><MFINFO><SECINFO><SECID><UNIQUEID>922906300</UNIQUEID><UNIQUEIDTYPE>CUSIP</UNIQUEIDTYPE></SECID><SECNAME>Vanguard Federal Money Market Fund</SECNAME><TICKER>VMFXX</TICKER><UNITPRICE>1.0</UNITPRICE><MEMO>Price as of date based on closing price</MEMO></SECINFO><MFTYPE>OPENEND</MFTYPE></MFINFO></SECLIST></SECLISTMSGSRSV1></OFX>

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-16"?>
<?OFX OFXHEADER="200" VERSION="203" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>
<OFX>
<SIGNONMSGSRSV1><SONRS>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<DTSERVER>20171126184401.091[0:GMT]</DTSERVER>
<LANGUAGE>ENG</LANGUAGE>
<FI>
<ORG>YCKVJ</ORG>
<FID>0351</FID>
</FI>
</SONRS></SIGNONMSGSRSV1>
<BANKMSGSRSV1>
<STMTTRNRS>
<TRNUID>0549c828-f02c-43c7-81a3-de0b3f23c393</TRNUID>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<STMTRS>
<CURDEF>USD</CURDEF>
<BANKACCTFROM>
<BANKID>115483849</BANKID>
<ACCTID>14839128817</ACCTID>
<ACCTTYPE>CHECKING</ACCTTYPE>
</BANKACCTFROM>
<BANKTRANLIST>
<DTSTART>20170828174401.637[0:GMT]</DTSTART>
<DTEND>20171126184401.637[0:GMT]</DTEND>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20170830120000.000[0:GMT]</DTPOSTED>
<TRNAMT>2843.08</TRNAMT>
<FITID>ce2cf749-dd15-4dc7-b78d-2f9e88d8a702</FITID>
<NAME>SALARY</NAME>
<MEMO>ACH Deposit 18181818199</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20170830120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-2354.66</TRNAMT>
<FITID>2bda11f4-9a9c-43fb-b67a-71f747dcf684</FITID>
<NAME>BILLPAY TO CREDIT CARD</NAME>
<MEMO>ACH Debit 8181819191919</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CHECK</TRNTYPE>
<DTPOSTED>20170830120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-15.00</TRNAMT>
<FITID>62dcc92d-ba0f-4fe6-8611-d6c1b86594fc</FITID>
<CHECKNUM>3304</CHECKNUM>
<NAME>Check</NAME>
<MEMO>INCLEARING CHECK</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171107120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-282.68</TRNAMT>
<FITID>29c74a94-f226-4980-b54c-da6fa2721d7e</FITID>
<NAME>DAYCARE o SIGONFILE</NAME>
<MEMO>ACH Debit 11818191919191</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171109120000.000[0:GMT]</DTPOSTED>
<TRNAMT>1300.98</TRNAMT>
<FITID>32e40e98-61c3-421c-acaa-55ae67a5f8fe</FITID>
<NAME>DIRECT DEPOSIT</NAME>
<MEMO>ACH Deposit 8282828282828</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171109120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-98.20</TRNAMT>
<FITID>4b73dbbf-aa27-4f62-b54a-ee0a9a3486d8</FITID>
<NAME>DUKEENGYPROGRESS DUKEENGYPR</NAME>
<MEMO>ACH Debit 017313004099621</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171115120000.000[0:GMT]</DTPOSTED>
<TRNAMT>1.01</TRNAMT>
<FITID>51c47252-4cf0-442c-b619-8a31b17ac489</FITID>
<NAME>Dividend Earned</NAME>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171116120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-51.75</TRNAMT>
<FITID>51cb12bb-cdd9-4333-8d8d-c423f9e8f833</FITID>
<NAME>TARGET DEBIT CRD ACH TRAN</NAME>
<MEMO>ACH Debit</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171120120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-25.18</TRNAMT>
<FITID>366a5b23-2f2e-4cf0-a714-6a306bd4e909</FITID>
<NAME>TARGET DEBIT CRD ACH TRAN</NAME>
<MEMO>ACH Debit</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171121120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-10.71</TRNAMT>
<FITID>9a463f21-c6e1-4fe0-b37b-f9a8cc942cf0</FITID>
<NAME>NETFLIX COM NETFLIX COM</NAME>
<MEMO>Point of Sale Debit L999 DATE 11-20</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171122120000.000[0:GMT]</DTPOSTED>
<TRNAMT>1300.98</TRNAMT>
<FITID>31f165e5-569f-4530-8438-a6ceb2301335</FITID>
<NAME>DIRECT DEPOSIT</NAME>
<MEMO>ACH Deposit 838383838383838</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171122120000.000[0:GMT]</DTPOSTED>
<TRNAMT>12.50</TRNAMT>
<FITID>215a10dd-f3a2-4336-ab8c-f22276cad552</FITID>
<NAME>CIRCLE INTERNET CIRCLE</NAME>
<MEMO>ACH Deposit 017326000283477</MEMO>
</STMTTRN>
</BANKTRANLIST>
<LEDGERBAL>
<BALAMT>2620.37</BALAMT>
<DTASOF>20171126184401.637[0:GMT]</DTASOF>
</LEDGERBAL>
<AVAILBAL>
<BALAMT>3620.37</BALAMT>
<DTASOF>20171126184401.637[0:GMT]</DTASOF>
</AVAILBAL>
</STMTRS></STMTTRNRS>
</BANKMSGSRSV1>
</OFX>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-16"?>
<?OFX OFXHEADER="200" VERSION="203" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>
<OFX>
<SIGNONMSGSRSV1><SONRS>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<DTSERVER>20171129025346.132[0:GMT]</DTSERVER>
<LANGUAGE>ENG</LANGUAGE>
<FI>
<ORG>YCKVJ</ORG>
<FID>0351</FID>
</FI>
</SONRS></SIGNONMSGSRSV1>
<BANKMSGSRSV1>
<STMTTRNRS>
<TRNUID>0549c828-f02c-43c7-81a3-de0b3f23c393</TRNUID>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<STMTRS>
<CURDEF>USD</CURDEF>
<BANKACCTFROM>
<BANKID>115483849</BANKID>
<ACCTID>14839128817</ACCTID>
<ACCTTYPE>CHECKING</ACCTTYPE>
</BANKACCTFROM>
<BANKTRANLIST>
<DTSTART>20170831174401.637[0:GMT]</DTSTART>
<DTEND>20171129184401.637[0:GMT]</DTEND>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171107120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-282.68</TRNAMT>
<FITID>29c74a94-f226-4980-b54c-da6fa2721d7e</FITID>
<NAME>DAYCARE o SIGONFILE</NAME>
<MEMO>ACH Debit 11818191919191</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171109120000.000[0:GMT]</DTPOSTED>
<TRNAMT>1300.98</TRNAMT>
<FITID>32e40e98-61c3-421c-acaa-55ae67a5f8fe</FITID>
<NAME>DIRECT DEPOSIT</NAME>
<MEMO>ACH Deposit 8282828282828</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171109120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-98.20</TRNAMT>
<FITID>4b73dbbf-aa27-4f62-b54a-ee0a9a3486d8</FITID>
<NAME>DUKEENGYPROGRESS DUKEENGYPR</NAME>
<MEMO>ACH Debit 017313004099621</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171115120000.000[0:GMT]</DTPOSTED>
<TRNAMT>1.01</TRNAMT>
<FITID>51c47252-4cf0-442c-b619-8a31b17ac489</FITID>
<NAME>Dividend Earned</NAME>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171116120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-51.75</TRNAMT>
<FITID>51cb12bb-cdd9-4333-8d8d-c423f9e8f833</FITID>
<NAME>TARGET DEBIT CRD ACH TRAN</NAME>
<MEMO>ACH Debit</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171120120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-25.18</TRNAMT>
<FITID>366a5b23-2f2e-4cf0-a714-6a306bd4e909</FITID>
<NAME>TARGET DEBIT CRD ACH TRAN</NAME>
<MEMO>ACH Debit</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20171121120000.000[0:GMT]</DTPOSTED>
<TRNAMT>-10.71</TRNAMT>
<FITID>9a463f21-c6e1-4fe0-b37b-f9a8cc942cf0</FITID>
<NAME>NETFLIX COM NETFLIX COM</NAME>
<MEMO>Point of Sale Debit L999 DATE 11-20</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171122120000.000[0:GMT]</DTPOSTED>
<TRNAMT>1300.98</TRNAMT>
<FITID>31f165e5-569f-4530-8438-a6ceb2301335</FITID>
<NAME>DIRECT DEPOSIT</NAME>
<MEMO>ACH Deposit 838383838383838</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171122120000.000[0:GMT]</DTPOSTED>
<TRNAMT>12.50</TRNAMT>
<FITID>215a10dd-f3a2-4336-ab8c-f22276cad552</FITID>
<NAME>CIRCLE INTERNET CIRCLE</NAME>
<MEMO>ACH Deposit 017326000283477</MEMO>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20171129120000.000[0:GMT]</DTPOSTED>
<TRNAMT>2843.08</TRNAMT>
<FITID>9a52df4b-3a8d-41bb-9141-96e1e3f294cf</FITID>
<NAME>SALARY</NAME>
<MEMO>ACH Deposit 18181818199</MEMO>
</STMTTRN>
</BANKTRANLIST>
<LEDGERBAL>
<BALAMT>5463.45</BALAMT>
<DTASOF>20171129025346.132[0:GMT]</DTASOF>
</LEDGERBAL>
<AVAILBAL>
<BALAMT>6463.45</BALAMT>
<DTASOF>20171129025346.132[0:GMT]</DTASOF>
</AVAILBAL>
</STMTRS></STMTTRNRS>
</BANKMSGSRSV1>
</OFX>

View File

@ -0,0 +1,11 @@
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>SUCCESS</STATUS><DTSERVER>20171128054239.013[-5:EST]<LANGUAGE>ENG<FI><ORG>C2<FID>29292</FI></SONRS></SIGNONMSGSRSV1><CREDITCARDMSGSRSV1><CCSTMTTRNRS><TRNUID>1cc61e4b-1f74-7d9a-b143-b8c80d5fda58<STATUS><CODE>0<SEVERITY>INFO</STATUS><CCSTMTRS><CURDEF>USD<CCACCTFROM><ACCTID>1234123412341234</CCACCTFROM><BANKTRANLIST><DTSTART>20170731054239.277[-4:EDT]<DTEND>20171128054239.277[-5:EST]<STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20171016120000[0:GMT]<TRNAMT>-99.98<FITID>2017101624445727288300440999736<NAME>KROGER #111</STMTTRN><STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170910120000[0:GMT]<TRNAMT>-150<FITID>2017091024493987251438675718282<NAME>CHARITY DONATION</STMTTRN><STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170814120000[0:GMT]<TRNAMT>-44.99<FITID>2017081424692167225100642481235<NAME>CABLE</STMTTRN><STMTTRN><TRNTYPE>CREDIT<DTPOSTED>20171101120000[0:GMT]<TRNAMT>185.71<FITID>2017110123053057200000291455612<NAME>Payment Thank You Electro</STMTTRN><STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20171016120000[0:GMT]<TRNAMT>-4.49<FITID>2017101624510727289100677772726<NAME>CRAFTS</STMTTRN><STMTTRN><TRNTYPE>CREDIT<DTPOSTED>20170815120000[0:GMT]<TRNAMT>109.26<FITID>2017081574692167226100322807539<NAME>Example.com</STMTTRN></BANKTRANLIST><LEDGERBAL><BALAMT>-4.49<DTASOF>20171128070000.000[-5:EST]</LEDGERBAL><AVAILBAL><BALAMT>995.51<DTASOF>20171128070000.000[-5:EST]</AVAILBAL></CCSTMTRS></CCSTMTTRNRS></CREDITCARDMSGSRSV1></OFX>

Binary file not shown.

View File

@ -0,0 +1,480 @@
package integration_test
import (
"encoding/json"
"fmt"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"strings"
"testing"
"time"
)
// Needed because handlers.User doesn't allow Password to be written to JSON
type User struct {
UserId int64
DefaultCurrency int64 // SecurityId of default currency, or ISO4217 code for it if creating new user
Name string
Username string
Password string
PasswordHash string
Email string
}
func (u *User) Write(w http.ResponseWriter) error {
enc := json.NewEncoder(w)
return enc.Encode(u)
}
func (u *User) Read(json_str string) error {
dec := json.NewDecoder(strings.NewReader(json_str))
return dec.Decode(u)
}
// TestData
type TestData struct {
initialized bool
users []User
clients []*http.Client
securities []models.Security
prices []models.Price
accounts []models.Account // accounts must appear after their parents in this slice
transactions []models.Transaction
reports []models.Report
tabulations []models.Tabulation
}
type TestDataFunc func(*testing.T, *TestData)
func (t *TestData) initUser(user *User, userid int) error {
newuser, err := createUser(user)
if err != nil {
return err
}
t.users = append(t.users, *newuser)
// make a copy of the user so we can set the password for creating the
// session without disturbing the original
userWithPassword := *newuser
userWithPassword.Password = user.Password
client, err := newSession(&userWithPassword)
if err != nil {
return err
}
t.clients = append(t.clients, client)
return nil
}
// Initialize makes requests to the server to create all of the objects
// represented in it before returning a copy of the data, with all of the *Id
// fields updated to their actual values
func (t *TestData) Initialize() (*TestData, error) {
var t2 TestData
for userid, user := range t.users {
err := t2.initUser(&user, userid)
if err != nil {
return nil, err
}
}
for _, security := range t.securities {
s2, err := createSecurity(t2.clients[security.UserId], &security)
if err != nil {
return nil, err
}
t2.securities = append(t2.securities, *s2)
}
for _, price := range t.prices {
userid := t.securities[price.SecurityId].UserId
price.SecurityId = t2.securities[price.SecurityId].SecurityId
price.CurrencyId = t2.securities[price.CurrencyId].SecurityId
p2, err := createPrice(t2.clients[userid], &price)
if err != nil {
return nil, err
}
t2.prices = append(t2.prices, *p2)
}
for _, account := range t.accounts {
account.SecurityId = t2.securities[account.SecurityId].SecurityId
if account.ParentAccountId != -1 {
account.ParentAccountId = t2.accounts[account.ParentAccountId].AccountId
}
a2, err := createAccount(t2.clients[account.UserId], &account)
if err != nil {
return nil, err
}
t2.accounts = append(t2.accounts, *a2)
}
for i, transaction := range t.transactions {
transaction.Splits = []*models.Split{}
for _, s := range t.transactions[i].Splits {
// Make a copy of the split since Splits is a slice of pointers so
// copying the transaction doesn't
split := *s
split.AccountId = t2.accounts[split.AccountId].AccountId
transaction.Splits = append(transaction.Splits, &split)
}
tt2, err := createTransaction(t2.clients[transaction.UserId], &transaction)
if err != nil {
return nil, err
}
t2.transactions = append(t2.transactions, *tt2)
}
for _, report := range t.reports {
r2, err := createReport(t2.clients[report.UserId], &report)
if err != nil {
return nil, err
}
t2.reports = append(t2.reports, *r2)
}
t2.initialized = true
return &t2, nil
}
func (t *TestData) Teardown() error {
if !t.initialized {
return fmt.Errorf("Cannot teardown uninitialized TestData")
}
for userid, user := range t.users {
err := deleteUser(t.clients[userid], &user)
if err != nil {
return err
}
}
return nil
}
var data = []TestData{
{
users: []User{
{
DefaultCurrency: 840, // USD
Name: "John Smith",
Username: "jsmith",
Password: "hunter2",
Email: "jsmith@example.com",
},
{
DefaultCurrency: 978, // Euro
Name: "Billy Bob",
Username: "bbob6",
Password: "#)$&!KF(*ADAHK#@*(FAJSDkalsdf98af32klhf98sd8a'2938LKJD",
Email: "bbob+moneygo@my-domain.com",
},
},
securities: []models.Security{
{
UserId: 0,
Name: "USD",
Description: "US Dollar",
Symbol: "$",
Precision: 2,
Type: models.Currency,
AlternateId: "840",
},
{
UserId: 0,
Name: "SPY",
Description: "SPDR S&P 500 ETF Trust",
Symbol: "SPY",
Precision: 5,
Type: models.Stock,
AlternateId: "78462F103",
},
{
UserId: 1,
Name: "EUR",
Description: "Euro",
Symbol: "€",
Precision: 2,
Type: models.Currency,
AlternateId: "978",
},
{
UserId: 0,
Name: "EUR",
Description: "Euro",
Symbol: "€",
Precision: 2,
Type: models.Currency,
AlternateId: "978",
},
},
prices: []models.Price{
{
SecurityId: 1,
CurrencyId: 0,
Date: time.Date(2017, time.January, 2, 21, 0, 0, 0, time.UTC),
Value: "225.24",
RemoteId: "12387-129831-1238",
},
{
SecurityId: 1,
CurrencyId: 0,
Date: time.Date(2017, time.January, 3, 21, 0, 0, 0, time.UTC),
Value: "226.58",
RemoteId: "12387-129831-1239",
},
{
SecurityId: 1,
CurrencyId: 0,
Date: time.Date(2017, time.January, 4, 21, 0, 0, 0, time.UTC),
Value: "226.40",
RemoteId: "12387-129831-1240",
},
{
SecurityId: 1,
CurrencyId: 0,
Date: time.Date(2017, time.January, 5, 21, 0, 0, 0, time.UTC),
Value: "227.21",
RemoteId: "12387-129831-1241",
},
{
SecurityId: 0,
CurrencyId: 3,
Date: time.Date(2017, time.November, 16, 18, 49, 53, 0, time.UTC),
Value: "0.85",
RemoteId: "USDEUR819298714",
},
},
accounts: []models.Account{
{
UserId: 0,
SecurityId: 0,
ParentAccountId: -1,
Type: models.Asset,
Name: "Assets",
},
{
UserId: 0,
SecurityId: 0,
ParentAccountId: 0,
Type: models.Bank,
Name: "Credit Union Checking",
},
{
UserId: 0,
SecurityId: 0,
ParentAccountId: -1,
Type: models.Expense,
Name: "Expenses",
},
{
UserId: 0,
SecurityId: 0,
ParentAccountId: 2,
Type: models.Expense,
Name: "Groceries",
},
{
UserId: 0,
SecurityId: 0,
ParentAccountId: 2,
Type: models.Expense,
Name: "Cable",
},
{
UserId: 1,
SecurityId: 2,
ParentAccountId: -1,
Type: models.Asset,
Name: "Assets",
},
{
UserId: 1,
SecurityId: 2,
ParentAccountId: -1,
Type: models.Expense,
Name: "Expenses",
},
{
UserId: 0,
SecurityId: 0,
ParentAccountId: -1,
Type: models.Liability,
Name: "Credit Card",
},
},
transactions: []models.Transaction{
{
UserId: 0,
Description: "weekly groceries",
Date: time.Date(2017, time.October, 15, 1, 16, 59, 0, time.UTC),
Splits: []*models.Split{
{
Status: models.Reconciled,
AccountId: 1,
SecurityId: -1,
Amount: "-5.6",
},
{
Status: models.Reconciled,
AccountId: 3,
SecurityId: -1,
Amount: "5.6",
},
},
},
{
UserId: 0,
Description: "weekly groceries",
Date: time.Date(2017, time.October, 31, 19, 10, 14, 0, time.UTC),
Splits: []*models.Split{
{
Status: models.Reconciled,
AccountId: 1,
SecurityId: -1,
Amount: "-81.59",
},
{
Status: models.Reconciled,
AccountId: 3,
SecurityId: -1,
Amount: "81.59",
},
},
},
{
UserId: 0,
Description: "Cable",
Date: time.Date(2017, time.September, 2, 0, 00, 00, 0, time.UTC),
Splits: []*models.Split{
{
Status: models.Reconciled,
AccountId: 1,
SecurityId: -1,
Amount: "-39.99",
},
{
Status: models.Entered,
AccountId: 4,
SecurityId: -1,
Amount: "39.99",
},
},
},
{
UserId: 1,
Description: "Gas",
Date: time.Date(2017, time.November, 1, 13, 19, 50, 0, time.UTC),
Splits: []*models.Split{
{
Status: models.Reconciled,
AccountId: 5,
SecurityId: -1,
Amount: "-24.56",
},
{
Status: models.Entered,
AccountId: 6,
SecurityId: -1,
Amount: "24.56",
},
},
},
},
reports: []models.Report{
{
UserId: 0,
Name: "This Year's Monthly Expenses",
Lua: `
function account_series_map(accounts, tabulation)
map = {}
for i=1,100 do -- we're not messing with accounts more than 100 levels deep
all_handled = true
for id, acct in pairs(accounts) do
if not map[id] then
all_handled = false
if not acct.parent then
map[id] = tabulation:series(acct.name)
elseif map[acct.parent.accountid] then
map[id] = map[acct.parent.accountid]:series(acct.name)
end
end
end
if all_handled then
return map
end
end
error("Accounts nested (at least) 100 levels deep")
end
function generate()
year = 2017
account_type = account.Expense
accounts = get_accounts()
t = tabulation.new(12)
t:title(year .. " Monthly Expenses")
t:subtitle("This is my subtitle")
t:units(get_default_currency().Symbol)
series_map = account_series_map(accounts, t)
for month=1,12 do
begin_date = date.new(year, month, 1)
end_date = date.new(year, month+1, 1)
t:label(month, tostring(begin_date))
for id, acct in pairs(accounts) do
series = series_map[id]
if acct.type == account_type then
balance = acct:balance(begin_date, end_date)
series:value(month, balance.amount)
end
end
end
return t
end`,
},
},
tabulations: []models.Tabulation{
{
ReportId: 0,
Title: "2017 Monthly Expenses",
Subtitle: "This is my subtitle",
Units: "USD",
Labels: []string{"2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01"},
Series: map[string]*models.Series{
"Assets": {
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Series: map[string]*models.Series{
"Credit Union Checking": {
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Series: map[string]*models.Series{},
},
},
},
"Expenses": {
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Series: map[string]*models.Series{
"Groceries": {
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 87.19, 0, 0},
Series: map[string]*models.Series{},
},
"Cable": {
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 39.99, 0, 0, 0},
Series: map[string]*models.Series{},
},
},
},
"Credit Card": {
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
Series: map[string]*models.Series{},
},
},
},
},
},
}

View File

@ -0,0 +1,492 @@
package integration_test
import (
"fmt"
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http"
"net/url"
"strconv"
"testing"
"time"
)
func createTransaction(client *http.Client, transaction *models.Transaction) (*models.Transaction, error) {
var s models.Transaction
err := create(client, transaction, &s, "/v1/transactions/")
return &s, err
}
func getTransaction(client *http.Client, transactionid int64) (*models.Transaction, error) {
var s models.Transaction
err := read(client, &s, "/v1/transactions/"+strconv.FormatInt(transactionid, 10))
if err != nil {
return nil, err
}
return &s, nil
}
func getTransactions(client *http.Client) (*models.TransactionList, error) {
var tl models.TransactionList
err := read(client, &tl, "/v1/transactions/")
if err != nil {
return nil, err
}
return &tl, nil
}
func getAccountTransactions(client *http.Client, accountid, page, limit int64, sort string) (*models.AccountTransactionsList, error) {
var atl models.AccountTransactionsList
params := url.Values{}
query := fmt.Sprintf("/v1/accounts/%d/transactions/", accountid)
if page != 0 {
params.Set("page", fmt.Sprintf("%d", page))
}
if limit != 0 {
params.Set("limit", fmt.Sprintf("%d", limit))
}
if len(sort) != 0 {
params.Set("sort", sort)
query += "?" + params.Encode()
}
err := read(client, &atl, query)
if err != nil {
return nil, err
}
return &atl, nil
}
func updateTransaction(client *http.Client, transaction *models.Transaction) (*models.Transaction, error) {
var s models.Transaction
err := update(client, transaction, &s, "/v1/transactions/"+strconv.FormatInt(transaction.TransactionId, 10))
if err != nil {
return nil, err
}
return &s, nil
}
func deleteTransaction(client *http.Client, s *models.Transaction) error {
err := remove(client, "/v1/transactions/"+strconv.FormatInt(s.TransactionId, 10))
if err != nil {
return err
}
return nil
}
func ensureTransactionsMatch(t *testing.T, expected, tran *models.Transaction, accounts *[]models.Account, matchtransactionids, matchsplitids bool) {
t.Helper()
if tran.TransactionId == 0 {
t.Errorf("TransactionId is 0")
}
if matchtransactionids && tran.TransactionId != expected.TransactionId {
t.Errorf("TransactionId (%d) doesn't match what's expected (%d)", tran.TransactionId, expected.TransactionId)
}
if tran.Description != expected.Description {
t.Errorf("Description doesn't match")
}
if !tran.Date.Equal(expected.Date) {
t.Errorf("Date (%+v) differs from expected (%+v)", tran.Date, expected.Date)
}
if len(tran.Splits) != len(expected.Splits) {
t.Fatalf("Expected %d splits, received %d", len(expected.Splits), len(tran.Splits))
}
foundIds := make(map[int64]bool)
for j := 0; j < len(expected.Splits); j++ {
origsplit := expected.Splits[j]
if tran.Splits[j].TransactionId != tran.TransactionId {
t.Fatalf("Split TransactionId doesn't match transaction's")
}
found := false
for _, s := range tran.Splits {
if s.SplitId == 0 {
t.Errorf("Found SplitId that's 0")
}
accountid := origsplit.AccountId
if accounts != nil {
accountid = (*accounts)[accountid].AccountId
}
if origsplit.Status == s.Status &&
origsplit.ImportSplitType == s.ImportSplitType &&
s.AccountId == accountid &&
s.SecurityId == -1 &&
origsplit.RemoteId == origsplit.RemoteId &&
origsplit.Number == s.Number &&
origsplit.Memo == s.Memo &&
origsplit.Amount == s.Amount &&
(!matchsplitids || origsplit.SplitId == s.SplitId) {
if _, ok := foundIds[s.SplitId]; ok {
continue
}
foundIds[s.SplitId] = true
found = true
break
}
}
if !found {
t.Errorf("Unable to find matching split: %+v", origsplit)
}
}
}
func getAccountVersionMap(t *testing.T, client *http.Client, tran *models.Transaction) map[int64]*models.Account {
t.Helper()
accountMap := make(map[int64]*models.Account)
for _, split := range tran.Splits {
account, err := getAccount(client, split.AccountId)
if err != nil {
t.Fatalf("Error fetching split's account while updating transaction: %s\n", err)
}
accountMap[account.AccountId] = account
}
return accountMap
}
func checkAccountVersionsUpdated(t *testing.T, client *http.Client, accountMap map[int64]*models.Account, tran *models.Transaction) {
for _, split := range tran.Splits {
account, err := getAccount(client, split.AccountId)
if err != nil {
t.Fatalf("Error fetching split's account after updating transaction: %s\n", err)
}
if account.AccountVersion <= accountMap[split.AccountId].AccountVersion {
t.Errorf("Failed to update account version when updating transaction split\n")
}
}
}
func TestCreateTransaction(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i, orig := range data[0].transactions {
transaction := d.transactions[i]
ensureTransactionsMatch(t, &orig, &transaction, &d.accounts, false, false)
accountMap := getAccountVersionMap(t, d.clients[orig.UserId], &transaction)
_, err := createTransaction(d.clients[orig.UserId], &transaction)
if err != nil {
t.Fatalf("Unxpected error creating transaction")
}
checkAccountVersionsUpdated(t, d.clients[orig.UserId], accountMap, &transaction)
}
// Don't allow imbalanced transactions
tran := models.Transaction{
UserId: d.users[0].UserId,
Description: "Imbalanced",
Date: time.Date(2017, time.September, 1, 0, 00, 00, 0, time.UTC),
Splits: []*models.Split{
{
Status: models.Reconciled,
AccountId: d.accounts[1].AccountId,
SecurityId: -1,
Amount: "-39.98",
},
{
Status: models.Entered,
AccountId: d.accounts[4].AccountId,
SecurityId: -1,
Amount: "39.99",
},
},
}
_, err := createTransaction(d.clients[0], &tran)
if err == nil {
t.Fatalf("Expected error creating imbalanced transaction")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error creating imbalanced transaction: %s", herr)
}
} else {
t.Fatalf("Unexpected error creating imbalanced transaction")
}
// Don't allow transactions with 0 splits
tran.Splits = []*models.Split{}
_, err = createTransaction(d.clients[0], &tran)
if err == nil {
t.Fatalf("Expected error creating with zero splits")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error creating with zero splits: %s", herr)
}
} else {
t.Fatalf("Unexpected error creating zero splits")
}
// Don't allow creating a transaction for another user
tran.UserId = d.users[1].UserId
_, err = createTransaction(d.clients[0], &tran)
if err == nil {
t.Fatalf("Expected error creating transaction for another user")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid request
t.Fatalf("Unexpected API error creating transction for another user: %s", herr)
}
} else {
t.Fatalf("Unexpected error creating transaction for another user")
}
})
}
func TestGetTransaction(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].transactions); i++ {
orig := data[0].transactions[i]
curr := d.transactions[i]
tran, err := getTransaction(d.clients[orig.UserId], curr.TransactionId)
if err != nil {
t.Fatalf("Error fetching transaction: %s\n", err)
}
ensureTransactionsMatch(t, &curr, tran, nil, true, true)
}
})
}
func TestGetTransactions(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
tl, err := getTransactions(d.clients[0])
if err != nil {
t.Fatalf("Error fetching transactions: %s\n", err)
}
numtransactions := 0
foundIds := make(map[int64]bool)
for i := 0; i < len(data[0].transactions); i++ {
orig := data[0].transactions[i]
curr := d.transactions[i]
if curr.UserId != d.users[0].UserId {
continue
}
numtransactions += 1
found := false
for _, tran := range *tl.Transactions {
if tran.TransactionId == curr.TransactionId {
ensureTransactionsMatch(t, &curr, tran, nil, true, true)
if _, ok := foundIds[tran.TransactionId]; ok {
continue
}
foundIds[tran.TransactionId] = true
found = true
break
}
}
if !found {
t.Errorf("Unable to find matching transaction: %+v", orig)
}
}
if numtransactions != len(*tl.Transactions) {
t.Fatalf("Expected %d transactions, received %d", numtransactions, len(*tl.Transactions))
}
})
}
func TestUpdateTransaction(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].transactions); i++ {
orig := data[0].transactions[i]
curr := d.transactions[i]
curr.Description = "more money"
curr.Date = time.Date(2017, time.October, 18, 10, 41, 40, 0, time.UTC)
accountMap := getAccountVersionMap(t, d.clients[orig.UserId], &curr)
tran, err := updateTransaction(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error updating transaction: %s\n", err)
}
checkAccountVersionsUpdated(t, d.clients[orig.UserId], accountMap, tran)
checkAccountVersionsUpdated(t, d.clients[orig.UserId], accountMap, &curr)
ensureTransactionsMatch(t, &curr, tran, nil, true, true)
tran.Splits = []*models.Split{}
for _, s := range curr.Splits {
var split models.Split
split = *s
tran.Splits = append(tran.Splits, &split)
}
// Don't allow updating transactions for other/invalid users
tran.UserId = tran.UserId + 1
tran2, err := updateTransaction(d.clients[orig.UserId], tran)
if tran2.UserId != curr.UserId {
t.Fatalf("Allowed updating transaction to have wrong UserId\n")
}
tran.UserId = curr.UserId
// Make sure we can't create an unbalanced transaction
tran.Splits[len(tran.Splits)-1].Amount = "42"
_, err = updateTransaction(d.clients[orig.UserId], tran)
if err == nil {
t.Fatalf("Expected error updating imbalanced transaction")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error updating imbalanced transaction: %s", herr)
}
} else {
t.Fatalf("Unexpected error updating imbalanced transaction")
}
// Don't allow transactions with 0 splits
tran.Splits = []*models.Split{}
_, err = updateTransaction(d.clients[orig.UserId], tran)
if err == nil {
t.Fatalf("Expected error updating with zero splits")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error updating with zero splits: %s", herr)
}
} else {
t.Fatalf("Unexpected error updating zero splits")
}
}
})
}
func TestDeleteTransaction(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for i := 0; i < len(data[0].transactions); i++ {
orig := data[0].transactions[i]
curr := d.transactions[i]
accountMap := getAccountVersionMap(t, d.clients[orig.UserId], &curr)
err := deleteTransaction(d.clients[orig.UserId], &curr)
if err != nil {
t.Fatalf("Error deleting transaction: %s\n", err)
}
checkAccountVersionsUpdated(t, d.clients[orig.UserId], accountMap, &curr)
_, err = getTransaction(d.clients[orig.UserId], curr.TransactionId)
if err == nil {
t.Fatalf("Expected error fetching deleted transaction")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 3 { // Invalid requeset
t.Fatalf("Unexpected API error fetching deleted transaction: %s", herr)
}
} else {
t.Fatalf("Unexpected error fetching deleted transaction")
}
}
})
}
func helperTestAccountTransactions(t *testing.T, d *TestData, account *models.Account, limit int64, sort string) {
if account.UserId != d.users[0].UserId {
return
}
var transactions []models.Transaction
var lastFetchCount int64
for page := int64(0); page == 0 || lastFetchCount > 0; page++ {
atl, err := getAccountTransactions(d.clients[0], account.AccountId, page, limit, sort)
if err != nil {
t.Fatalf("Error fetching account transactions: %s\n", err)
}
if limit != 0 && atl.Transactions != nil && int64(len(*atl.Transactions)) > limit {
t.Errorf("Exceeded limit of %d transactions (returned %d)\n", limit, len(*atl.Transactions))
}
if atl.Transactions != nil {
for _, tran := range *atl.Transactions {
transactions = append(transactions, *tran)
}
lastFetchCount = int64(len(*atl.Transactions))
} else {
lastFetchCount = -1
}
}
var lastDate time.Time
for _, tran := range transactions {
if lastDate.IsZero() {
lastDate = tran.Date
continue
} else if sort == "date-desc" && lastDate.Before(tran.Date) {
t.Errorf("Sorted by date-desc, but later transaction has later date")
} else if sort == "date-asc" && lastDate.After(tran.Date) {
t.Errorf("Sorted by date-asc, but later transaction has earlier date")
}
lastDate = tran.Date
}
numtransactions := 0
foundIds := make(map[int64]bool)
for i := 0; i < len(d.transactions); i++ {
curr := d.transactions[i]
if curr.UserId != d.users[0].UserId {
continue
}
// Don't consider this transaction if we didn't find a split
// for the account we're considering
account_found := false
for _, s := range curr.Splits {
if s.AccountId == account.AccountId {
account_found = true
break
}
}
if !account_found {
continue
}
numtransactions += 1
found := false
for _, tran := range transactions {
if tran.TransactionId == curr.TransactionId {
ensureTransactionsMatch(t, &curr, &tran, nil, true, true)
if _, ok := foundIds[tran.TransactionId]; ok {
continue
}
foundIds[tran.TransactionId] = true
found = true
break
}
}
if !found {
t.Errorf("Unable to find matching transaction: %+v", curr)
t.Errorf("Transactions: %+v\n", transactions)
}
}
if numtransactions != len(transactions) {
t.Fatalf("Expected %d transactions, received %d", numtransactions, len(transactions))
}
}
func TestAccountTransactions(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for _, account := range d.accounts {
helperTestAccountTransactions(t, d, &account, 0, "date-desc")
helperTestAccountTransactions(t, d, &account, 0, "date-asc")
helperTestAccountTransactions(t, d, &account, 1, "date-desc")
helperTestAccountTransactions(t, d, &account, 1, "date-asc")
helperTestAccountTransactions(t, d, &account, 2, "date-desc")
helperTestAccountTransactions(t, d, &account, 2, "date-asc")
}
})
}

View File

@ -0,0 +1,110 @@
package integration_test
import (
"github.com/aclindsa/moneygo/internal/handlers"
"net/http"
"strconv"
"testing"
)
func createUser(user *User) (*User, error) {
var u User
err := create(server.Client(), user, &u, "/v1/users/")
return &u, err
}
func getUser(client *http.Client, userid int64) (*User, error) {
var u User
err := read(client, &u, "/v1/users/"+strconv.FormatInt(userid, 10))
if err != nil {
return nil, err
}
return &u, nil
}
func updateUser(client *http.Client, user *User) (*User, error) {
var u User
err := update(client, user, &u, "/v1/users/"+strconv.FormatInt(user.UserId, 10))
if err != nil {
return nil, err
}
return &u, nil
}
func deleteUser(client *http.Client, u *User) error {
err := remove(client, "/v1/users/"+strconv.FormatInt(u.UserId, 10))
if err != nil {
return err
}
return nil
}
func TestCreateUser(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
if d.users[0].UserId == 0 || len(d.users[0].Username) == 0 {
t.Errorf("Unable to create user: %+v", data[0].users[0])
}
if len(d.users[0].Password) != 0 || len(d.users[0].PasswordHash) != 0 {
t.Error("Never send password, only send password hash when necessary")
}
})
}
func TestDontRecreateUser(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
for _, user := range data[0].users {
_, err := createUser(&user)
if err == nil {
t.Fatalf("Expected error re-creating user")
}
if herr, ok := err.(*handlers.Error); ok {
if herr.ErrorId != 4 { // User exists
t.Fatalf("Unexpected API error re-creating user: %s", herr)
}
} else {
t.Fatalf("Expected error re-creating user")
}
}
})
}
func TestGetUser(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
u, err := getUser(d.clients[0], d.users[0].UserId)
if err != nil {
t.Fatalf("Error fetching user: %s\n", err)
}
if u.UserId != d.users[0].UserId {
t.Errorf("UserId doesn't match")
}
if len(u.Username) == 0 {
t.Fatalf("Empty username for: %d", d.users[0].UserId)
}
})
}
func TestUpdateUser(t *testing.T) {
RunWith(t, &data[0], func(t *testing.T, d *TestData) {
user := &d.users[0]
user.Name = "Bob"
user.Email = "bob@example.com"
u, err := updateUser(d.clients[0], user)
if err != nil {
t.Fatalf("Error updating user: %s\n", err)
}
if u.UserId != user.UserId {
t.Errorf("UserId doesn't match")
}
if u.Username != u.Username {
t.Errorf("Username doesn't match")
}
if u.Name != user.Name {
t.Errorf("Name doesn't match")
}
if u.Email != user.Email {
t.Errorf("Email doesn't match")
}
})
}