Gonçalo Rodrigues 13b7149614 First Commit
2026-06-13 11:25:23 +01:00

311 lines
8.5 KiB
Go

package main
import (
"os"
"strings"
"testing"
)
func TestParseCents(t *testing.T) {
tests := []struct {
name string
input string
decSep string
want int64
wantErr bool
}{
{"positive whole", "100", ".", 10000, false},
{"positive decimal", "100.50", ".", 10050, false},
{"negative dash", "-50.25", ".", -5025, false},
{"negative parens", "(50.25)", ".", -5025, false},
{"euro symbol", "€1.234,56", ",", 123456, false},
{"euro negative parens", "(€1.234,56)", ",", -123456, false},
{"thousands separator", "1,234.56", ".", 123456, false},
{"space thousand", "1 234.56", ".", 123456, false},
{"nb space thousand", "1\u00a0234.56", ".", 123456, false},
{"zero", "0", ".", 0, false},
{"negative zero", "-0", ".", 0, false},
{"zero cents", "100.00", ".", 10000, false},
{"comma decimal", "1234,56", ",", 123456, false},
{"comma decimal with dot thousand", "1.234,56", ",", 123456, false},
{"fractional cent rounds up", "19.99", ".", 1999, false},
{"fractional cent rounds down", "10.01", ".", 1001, false},
{"euro fractional cent", "-19,99", ",", -1999, false},
{"empty string", "", ".", 0, true},
{"invalid", "abc", ".", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseCents(tt.input, tt.decSep)
if (err != nil) != tt.wantErr {
t.Errorf("parseCents(%q, %q) error = %v, wantErr %v", tt.input, tt.decSep, err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseCents(%q, %q) = %d, want %d", tt.input, tt.decSep, got, tt.want)
}
})
}
}
func TestParseCSV_CGD(t *testing.T) {
tests := []struct {
name string
file string
wantRows int
check func(*testing.T, []CSVImportRow)
}{
{
name: "basic CGD",
file: "testdata/cgd_basic.csv",
wantRows: 7,
check: func(t *testing.T, rows []CSVImportRow) {
if rows[0].Description != "Supermercado Pingo Doce" {
t.Errorf("first desc = %q", rows[0].Description)
}
if rows[0].AmountCents != -5490 {
t.Errorf("first amount = %d, want -5490 (debit)", rows[0].AmountCents)
}
if rows[0].Date != "2024-01-02" {
t.Errorf("first date = %q", rows[0].Date)
}
if rows[4].AmountCents != 250000 {
t.Errorf("salary (credit) = %d, want 250000", rows[4].AmountCents)
}
if rows[6].AmountCents != -8999 {
t.Errorf("zara (debit) = %d, want -8999", rows[6].AmountCents)
}
},
},
{
name: "real CGD download",
file: "testdata/comprovativo.csv",
wantRows: 36,
check: func(t *testing.T, rows []CSVImportRow) {
if rows[0].Description != "EA ELECTRONIC ARTS" {
t.Errorf("first desc = %q", rows[0].Description)
}
if rows[0].AmountCents != -2399 {
t.Errorf("ea arts (debit) = %d, want -2399", rows[0].AmountCents)
}
if rows[2].AmountCents != 200000 {
t.Errorf("trf marta (credit) = %d, want 200000", rows[2].AmountCents)
}
if rows[5].AmountCents != 200000 {
t.Errorf("second marta (credit) = %d, want 200000", rows[5].AmountCents)
}
if rows[6].AmountCents != -11998 {
t.Errorf("worten (debit) = %d, want -11998", rows[6].AmountCents)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(tt.file)
if err != nil {
t.Fatal(err)
}
rows, err := parseCSV(strings.NewReader(string(data)), CGDMapping)
if err != nil {
t.Fatal(err)
}
if len(rows) != tt.wantRows {
t.Fatalf("got %d rows, want %d", len(rows), tt.wantRows)
}
if tt.check != nil {
tt.check(t, rows)
}
})
}
}
func TestParseCSV_TradeRepublic(t *testing.T) {
tests := []struct {
name string
file string
wantRows int
check func(*testing.T, []CSVImportRow)
}{
{
name: "TR transaction export",
file: "testdata/traderepublic_card.csv",
wantRows: 5,
check: func(t *testing.T, rows []CSVImportRow) {
if rows[0].AmountCents != 5000 {
t.Errorf("first transfer = %d, want 5000", rows[0].AmountCents)
}
if rows[4].AmountCents != 30000 {
t.Errorf("second transfer = %d, want 30000", rows[4].AmountCents)
}
if rows[0].Date != "2025-12-11" {
t.Errorf("first date = %q", rows[0].Date)
}
if rows[0].Description != "Incoming transfer from GONCALO GOMES RODRIGUES" {
t.Errorf("first desc = %q", rows[0].Description)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(tt.file)
if err != nil {
t.Fatal(err)
}
rows, err := parseCSV(strings.NewReader(string(data)), TradeRepublicMapping)
if err != nil {
t.Fatal(err)
}
if len(rows) != tt.wantRows {
t.Fatalf("got %d rows, want %d", len(rows), tt.wantRows)
}
if tt.check != nil {
tt.check(t, rows)
}
})
}
}
func TestParseSecuritiesCSV_FromFile(t *testing.T) {
tests := []struct {
name string
file string
wantTrades int
check func(*testing.T, []securitiesTradeRow)
}{
{
name: "TR securities CSV",
file: "testdata/traderepublic_securities.csv",
wantTrades: 5,
check: func(t *testing.T, trades []securitiesTradeRow) {
if trades[0].ISIN != "IE00B3WJKG14" {
t.Errorf("first ISIN = %q", trades[0].ISIN)
}
if trades[0].Type != "buy" || trades[0].TotalCents != 3000 {
t.Errorf("first trade = %+v", trades[0])
}
if trades[2].ISIN != "IE00B5BMR087" || trades[2].TotalCents != 10000 {
t.Errorf("third trade = %+v", trades[2])
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(tt.file)
if err != nil {
t.Fatal(err)
}
trades, err := parseSecuritiesCSV(strings.NewReader(string(data)))
if err != nil {
t.Fatal(err)
}
if len(trades) != tt.wantTrades {
t.Fatalf("got %d trades, want %d", len(trades), tt.wantTrades)
}
if tt.check != nil {
tt.check(t, trades)
}
})
}
}
func TestParseCSV_EdgeCases(t *testing.T) {
t.Run("empty input", func(t *testing.T) {
_, err := parseCSV(strings.NewReader(""), CGDMapping)
if err == nil {
t.Fatal("expected error for empty input")
}
})
t.Run("all rows invalid", func(t *testing.T) {
csv := "Data;Desc;Amount\ninvalid;test;abc\n"
_, err := parseCSV(strings.NewReader(csv), CGDMapping)
if err == nil {
t.Fatal("expected error for all invalid rows")
}
})
t.Run("generic format detection", func(t *testing.T) {
csv := "date,description,amount\n2024-01-01,Test,10.50\n"
mapping := GenericMapping([]byte(csv))
if mapping.DateFormat != "2006-01-02" {
t.Errorf("date format = %q, want 2006-01-02", mapping.DateFormat)
}
rows, err := parseCSV(strings.NewReader(csv), mapping)
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 || rows[0].AmountCents != 1050 {
t.Errorf("generic parse = %+v", rows[0])
}
})
}
func TestParseCSV_TRFullExport(t *testing.T) {
data, err := os.ReadFile("testdata/Transaction export.csv")
if err != nil {
t.Fatal(err)
}
t.Run("transactions", func(t *testing.T) {
rows, err := parseCSV(strings.NewReader(string(data)), TradeRepublicMapping)
if err != nil {
t.Fatal(err)
}
if len(rows) != 18 {
t.Fatalf("got %d transaction rows, want 18", len(rows))
}
if rows[0].AmountCents != 5000 {
t.Errorf("first transfer = %d, want 5000", rows[0].AmountCents)
}
if rows[7].AmountCents != -10000 {
t.Errorf("row 7 = %d, want -10000 (buy -100.00)", rows[7].AmountCents)
}
})
t.Run("securities", func(t *testing.T) {
trades, err := parseSecuritiesCSV(strings.NewReader(string(data)))
if err != nil {
t.Fatal(err)
}
if len(trades) != 8 {
t.Fatalf("got %d trades, want 8", len(trades))
}
if trades[0].ISIN != "IE00B3WJKG14" || trades[0].Type != "buy" {
t.Errorf("first trade = %+v", trades[0])
}
})
}
func TestParseSecuritiesCSV_EdgeCases(t *testing.T) {
t.Run("missing required column", func(t *testing.T) {
csv := "Date,Name,Type,Quantity\n2024-01-01,Test,Buy,1\n"
_, err := parseSecuritiesCSV(strings.NewReader(csv))
if err == nil {
t.Fatal("expected error for missing columns")
}
})
t.Run("all zero rows skipped", func(t *testing.T) {
csv := "Date,Name,ISIN,Type,Quantity,Price,Total,Currency\n2024-01-01,,ISIN123,Buy,0,0,0,EUR\n"
_, err := parseSecuritiesCSV(strings.NewReader(csv))
if err == nil {
t.Fatal("expected error for all-zero rows")
}
})
t.Run("no data rows", func(t *testing.T) {
csv := "Date,Name,ISIN,Type,Quantity,Price,Total,Currency\n"
_, err := parseSecuritiesCSV(strings.NewReader(csv))
if err == nil {
t.Fatal("expected error for header-only CSV")
}
})
}