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:
77
internal/integration/accounts_lua_test.go
Normal file
77
internal/integration/accounts_lua_test.go
Normal 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)},
|
||||
})
|
||||
})
|
||||
}
|
222
internal/integration/accounts_test.go
Normal file
222
internal/integration/accounts_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
40
internal/integration/balance_lua_test.go
Normal file
40
internal/integration/balance_lua_test.go
Normal 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"},
|
||||
})
|
||||
})
|
||||
}
|
272
internal/integration/common_test.go
Normal file
272
internal/integration/common_test.go
Normal 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))
|
||||
}
|
31
internal/integration/date_lua_test.go
Normal file
31
internal/integration/date_lua_test.go
Normal 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"},
|
||||
})
|
||||
})
|
||||
}
|
129
internal/integration/gnucash_test.go
Normal file
129
internal/integration/gnucash_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
229
internal/integration/ofx_test.go
Normal file
229
internal/integration/ofx_test.go
Normal 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?
|
||||
})
|
||||
}
|
23
internal/integration/prices_lua_test.go
Normal file
23
internal/integration/prices_lua_test.go
Normal 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"},
|
||||
})
|
||||
})
|
||||
}
|
219
internal/integration/prices_test.go
Normal file
219
internal/integration/prices_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
46
internal/integration/reports_lua_test.go
Normal file
46
internal/integration/reports_lua_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
282
internal/integration/reports_test.go
Normal file
282
internal/integration/reports_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
87
internal/integration/securities_lua_test.go
Normal file
87
internal/integration/securities_lua_test.go
Normal 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)},
|
||||
})
|
||||
})
|
||||
}
|
267
internal/integration/securities_test.go
Normal file
267
internal/integration/securities_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
123
internal/integration/security_templates_test.go
Normal file
123
internal/integration/security_templates_test.go
Normal 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")
|
||||
}
|
||||
}
|
121
internal/integration/sessions_test.go
Normal file
121
internal/integration/sessions_test.go
Normal 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
|
||||
})
|
||||
}
|
1
internal/integration/testdata/401k_mutualfunds.ofx
vendored
Normal file
1
internal/integration/testdata/401k_mutualfunds.ofx
vendored
Normal 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>
|
11
internal/integration/testdata/brokerage.ofx
vendored
Normal file
11
internal/integration/testdata/brokerage.ofx
vendored
Normal 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>
|
140
internal/integration/testdata/checking_20171126.ofx
vendored
Normal file
140
internal/integration/testdata/checking_20171126.ofx
vendored
Normal 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>
|
123
internal/integration/testdata/checking_20171129.ofx
vendored
Normal file
123
internal/integration/testdata/checking_20171129.ofx
vendored
Normal 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>
|
11
internal/integration/testdata/creditcard.ofx
vendored
Normal file
11
internal/integration/testdata/creditcard.ofx
vendored
Normal 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>
|
BIN
internal/integration/testdata/example.gnucash
vendored
Normal file
BIN
internal/integration/testdata/example.gnucash
vendored
Normal file
Binary file not shown.
480
internal/integration/testdata_test.go
Normal file
480
internal/integration/testdata_test.go
Normal 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{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
492
internal/integration/transactions_test.go
Normal file
492
internal/integration/transactions_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
110
internal/integration/users_test.go
Normal file
110
internal/integration/users_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user