test: add unit tests — 70.3% coverage

Extracted storeIface from Handler so store can be mocked in tests.
Added handler_test.go with 98 test cases covering:
- All HTTP handlers (Dashboard, Transactions, Accounts, Categories, Goals,
  Portfolio, Reports, Projections, NetWorth, Simulator, Sharing, Import*)
- Auth middleware (authMW, ownerOrViewerMW)
- Pure helpers (monthsBetween, parseFloat, sortStrings, appendIfMissing)
- Error paths (bad JSON, missing fields, store errors, bad CSV)
- Alert logic (budget exceeded, goal miss, spend pace)
- Template funcmap exercised via rich render scenarios

Also added tickerStore.resolve tests to portfolio_test.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gonçalo Rodrigues 2026-06-13 17:18:49 +01:00
parent 995c6d89d6
commit 9dfc95cd32
3 changed files with 1657 additions and 1 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"context"
"embed"
"encoding/json"
"fmt"
@ -150,8 +151,37 @@ func render(w http.ResponseWriter, tmpl *template.Template, data interface{}) {
}
}
type storeIface interface {
getAccounts(ctx context.Context, userID string) ([]Account, error)
getAccount(ctx context.Context, id string) (*Account, error)
createAccount(ctx context.Context, a *Account) error
deleteAccount(ctx context.Context, id, userID string) error
getCategories(ctx context.Context, userID string) ([]Category, error)
createCategory(ctx context.Context, c *Category) error
updateCategory(ctx context.Context, c *Category) error
deleteCategory(ctx context.Context, id, userID string) error
getTransactions(ctx context.Context, userID string, filter bson.M) ([]Transaction, error)
getTransaction(ctx context.Context, id, userID string) (*Transaction, error)
createTransactions(ctx context.Context, txns []Transaction) error
updateTransaction(ctx context.Context, id, userID string, update bson.M) error
deleteTransaction(ctx context.Context, id, userID string) error
aggregateTransactions(ctx context.Context, userID string, pipeline bson.A) ([]bson.M, error)
getTrades(ctx context.Context, userID string) ([]Trade, error)
createTrades(ctx context.Context, trades []Trade) error
deleteTrade(ctx context.Context, id, userID string) error
getPermissions(ctx context.Context, ownerID string) ([]Permission, error)
getGrantedViewers(ctx context.Context, viewerID string) ([]Permission, error)
createPermission(ctx context.Context, p *Permission) error
deletePermission(ctx context.Context, ownerID, viewerID string) error
getGoals(ctx context.Context, userID string) ([]Goal, error)
createGoal(ctx context.Context, g *Goal) error
updateGoal(ctx context.Context, id, userID string, update bson.M) error
deleteGoal(ctx context.Context, id, userID string) error
seedCategories(ctx context.Context, userID string) error
}
type Handler struct {
store *Store
store storeIface
}
func NewHandler(store *Store) *Handler {

File diff suppressed because it is too large Load Diff

View File

@ -176,6 +176,27 @@ func TestTopMerchants(t *testing.T) {
}
}
func TestTickerStoreResolve(t *testing.T) {
ts := &tickerStore{mappings: []TickerMapping{
{ISIN: "IE00B3WJKG14", Ticker: "VWCE.DE"},
{ISIN: "US0378331005", Ticker: "AAPL"},
}}
tests := []struct {
isin string
want string
}{
{"IE00B3WJKG14", "VWCE.DE"},
{"US0378331005", "AAPL"},
{"XX0000000000", ""},
}
for _, tt := range tests {
got := ts.resolve(tt.isin)
if got != tt.want {
t.Errorf("resolve(%q) = %q, want %q", tt.isin, got, tt.want)
}
}
}
func TestAutoCategorize(t *testing.T) {
tests := []struct {
desc string