package database import ( "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testDSN = "file::memory:?cache=shared" // const testDSN = "/tmp/test.db" func TestCreateDB(t *testing.T) { _, err := New("sqlite", testDSN) require.NoError(t, err) _, err = New("IDoNotExist", testDSN) require.Error(t, err) } func TestAccountManagement(t *testing.T) { dbc, err := New("sqlite", testDSN) require.NoError(t, err) // Try to create invalid account type _, err = dbc.CreateAccount("test", AccountType("foobar")) require.Error(t, err) // Create account for testing and validate ID act, err := dbc.CreateAccount("test", AccountTypeBudget) assert.NoError(t, err) assert.NotEqual(t, uuid.Nil, act.ID) // Store ID actID := act.ID // Fetch account by ID act, err = dbc.GetAccount(actID) assert.NoError(t, err) assert.Equal(t, actID, act.ID) assert.Equal(t, "test", act.Name) // List all accounts accs, err := dbc.ListAccounts(false) assert.NoError(t, err) assert.Len(t, accs, 2) // Hide account and list again assert.NoError(t, dbc.UpdateAccountHidden(actID, true)) accs, err = dbc.ListAccounts(false) assert.NoError(t, err) assert.Len(t, accs, 1) // Unhide account and list again assert.NoError(t, dbc.UpdateAccountHidden(actID, false)) accs, err = dbc.ListAccounts(false) assert.NoError(t, err) assert.Len(t, accs, 2) // List accounts from other type accs, err = dbc.ListAccountsByType(AccountTypeCategory, false) assert.NoError(t, err) assert.Len(t, accs, 1) // List accounts from existing type accs, err = dbc.ListAccountsByType(AccountTypeBudget, false) assert.NoError(t, err) assert.Len(t, accs, 1) // Rename account assert.NoError(t, dbc.UpdateAccountName(actID, "renamed")) act, err = dbc.GetAccount(actID) assert.NoError(t, err) assert.Equal(t, actID, act.ID) assert.Equal(t, "renamed", act.Name) } func TestPairKeyRemoval(t *testing.T) { dbc, err := New("sqlite", testDSN) require.NoError(t, err) // Create two accounts to transfer from / to tb1, err := dbc.CreateAccount("test1", AccountTypeBudget) require.NoError(t, err) tb2, err := dbc.CreateAccount("test2", AccountTypeBudget) require.NoError(t, err) // Lets verify both of them do have zero-balance bals, err := dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 0) testCheckAcctBal(t, bals, tb2.ID, 0) // Transfer some money require.NoError(t, dbc.TransferMoney(tb1.ID, tb2.ID, 500)) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, -500) testCheckAcctBal(t, bals, tb2.ID, 500) // Now fetch the transactions on one of the accounts and delete the only one txs, err := dbc.ListTransactionsByAccount(tb1.ID, time.Time{}, time.Now()) require.NoError(t, err) require.Len(t, txs, 1) // Should only be one by now require.NoError(t, dbc.DeleteTransaction(txs[0].ID)) // Check both accounts went back to zero-balance (so paired tx are gone) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 0) testCheckAcctBal(t, bals, tb2.ID, 0) } //nolint:funlen func TestTransactions(t *testing.T) { dbc, err := New("sqlite", testDSN) require.NoError(t, err) // Set up some accounts for testing tb1, err := dbc.CreateAccount("test1", AccountTypeBudget) require.NoError(t, err) tb2, err := dbc.CreateAccount("test2", AccountTypeBudget) require.NoError(t, err) tt, err := dbc.CreateAccount("test", AccountTypeTracking) require.NoError(t, err) tc, err := dbc.CreateAccount("test", AccountTypeCategory) require.NoError(t, err) // Try to enter an invalid tx _, err = dbc.CreateTransaction(Transaction{ Payee: "ACME Inc.", Description: "Monthly Income", Amount: 1000, Account: uuid.NullUUID{UUID: tb1.ID, Valid: true}, Category: uuid.NullUUID{UUID: UnallocatedMoney, Valid: true}, Cleared: true, }) require.Error(t, err) // Lets earn some money tx, err := dbc.CreateTransaction(Transaction{ Time: time.Now(), Payee: "ACME Inc.", Description: "Monthly Income", Amount: 1000, Account: uuid.NullUUID{UUID: tb1.ID, Valid: true}, Category: uuid.NullUUID{UUID: UnallocatedMoney, Valid: true}, Cleared: true, }) require.NoError(t, err) assert.NotEqual(t, uuid.Nil, tx.ID) // Now we should have money… bals, err := dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 1000) testCheckAcctBal(t, bals, tb2.ID, 0) testCheckAcctBal(t, bals, tt.ID, 0) testCheckAcctBal(t, bals, tc.ID, 0) testCheckAcctBal(t, bals, UnallocatedMoney, 1000) // Lets redistribute the money require.NoError(t, dbc.TransferMoney(UnallocatedMoney, tc.ID, 500)) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 1000) testCheckAcctBal(t, bals, tb2.ID, 0) testCheckAcctBal(t, bals, tt.ID, 0) testCheckAcctBal(t, bals, tc.ID, 500) testCheckAcctBal(t, bals, UnallocatedMoney, 500) // Now transfer some money to another budget account require.NoError(t, dbc.TransferMoney(tb1.ID, tb2.ID, 100)) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 900) testCheckAcctBal(t, bals, tb2.ID, 100) testCheckAcctBal(t, bals, tt.ID, 0) testCheckAcctBal(t, bals, tc.ID, 500) testCheckAcctBal(t, bals, UnallocatedMoney, 500) // And some to a tracking account (needs category) require.NoError(t, dbc.TransferMoneyWithCategory(tb1.ID, tt.ID, 100, tc.ID)) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 800) testCheckAcctBal(t, bals, tb2.ID, 100) testCheckAcctBal(t, bals, tt.ID, 100) testCheckAcctBal(t, bals, tc.ID, 400) testCheckAcctBal(t, bals, UnallocatedMoney, 500) // We might also spend money lltx, err := dbc.CreateTransaction(Transaction{ Time: time.Now(), Payee: "Landlord", Description: "Rent", Amount: -100, Account: uuid.NullUUID{UUID: tb1.ID, Valid: true}, Category: uuid.NullUUID{UUID: tc.ID, Valid: true}, Cleared: false, }) require.NoError(t, err) assert.False(t, lltx.Cleared) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 700) testCheckAcctBal(t, bals, tb2.ID, 100) testCheckAcctBal(t, bals, tt.ID, 100) testCheckAcctBal(t, bals, tc.ID, 300) testCheckAcctBal(t, bals, UnallocatedMoney, 500) // List transactions txs, err := dbc.ListTransactionsByAccount(tb1.ID, time.Time{}, time.Now()) require.NoError(t, err) assert.Len(t, txs, 4) txs, err = dbc.ListTransactionsByAccount(UnallocatedMoney, time.Time{}, time.Now()) require.NoError(t, err) assert.Len(t, txs, 2) // Oh, wrong category require.NoError(t, dbc.UpdateTransactionCategory(lltx.ID, UnallocatedMoney)) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 700) testCheckAcctBal(t, bals, tb2.ID, 100) testCheckAcctBal(t, bals, tt.ID, 100) testCheckAcctBal(t, bals, tc.ID, 400) testCheckAcctBal(t, bals, UnallocatedMoney, 400) // Lets try to move it to a broken category require.Error(t, dbc.UpdateTransactionCategory(lltx.ID, tt.ID)) // Lets try to move an account instead of a tx require.Error(t, dbc.UpdateTransactionCategory(tb1.ID, UnallocatedMoney)) // Clear the tx require.NoError(t, dbc.UpdateTransactionCleared(lltx.ID, true)) lltx, err = dbc.GetTransactionByID(lltx.ID) require.NoError(t, err) assert.True(t, lltx.Cleared) // We made an error and didn't pay the landlord require.NoError(t, dbc.DeleteTransaction(lltx.ID)) bals, err = dbc.ListAccountBalances(false) require.NoError(t, err) testCheckAcctBal(t, bals, tb1.ID, 800) testCheckAcctBal(t, bals, tb2.ID, 100) testCheckAcctBal(t, bals, tt.ID, 100) testCheckAcctBal(t, bals, tc.ID, 400) testCheckAcctBal(t, bals, UnallocatedMoney, 500) // Get a deleted transaction _, err = dbc.GetTransactionByID(lltx.ID) require.Error(t, err) // List transactions txs, err = dbc.ListTransactionsByAccount(tb1.ID, time.Time{}, time.Now()) require.NoError(t, err) assert.Len(t, txs, 3) } func testCheckAcctBal(t *testing.T, bals []AccountBalance, act uuid.UUID, bal float64) { for _, b := range bals { if b.ID == act { assert.Equal(t, bal, b.Balance) return } } t.Errorf("account %s balance not found", act) }