package server import ( "context" "encoding/json" "fmt" "io/fs" "net/http" "os" "path/filepath" "strconv" "strings" "testing" "time" "git.javil.eu/jacob1123/budgeteer/bcrypt" "git.javil.eu/jacob1123/budgeteer/config" "git.javil.eu/jacob1123/budgeteer/jwt" "git.javil.eu/jacob1123/budgeteer/postgres" "git.javil.eu/jacob1123/budgeteer/web" txdb "github.com/DATA-DOG/go-txdb" ) var cfg = config.Config{ //nolint:gochecknoglobals DatabaseConnection: "postgres://budgeteer:budgeteer@db:5432/budgeteer_test", SessionSecret: "this_is_my_demo_secret_for_unit_tests", } func init() { //nolint:gochecknoinits _, err := postgres.Connect("pgx", cfg.DatabaseConnection) if err != nil { panic("failed connecting to DB for migrations: " + err.Error()) } txdb.Register("pgtx", "pgx", cfg.DatabaseConnection) } func TestMain(t *testing.T) { t.Parallel() queries, err := postgres.Connect("pgtx", cfg.DatabaseConnection) if err != nil { t.Errorf("connect to DB: %v", err) } static, err := fs.Sub(web.Static, "dist") if err != nil { panic("couldn't open static files") } tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret) if err != nil { panic(fmt.Errorf("couldn't create token verifier: %w", err)) } handler := &Handler{ Service: queries, TokenVerifier: tokenVerifier, CredentialsVerifier: &bcrypt.Verifier{}, StaticFS: http.FS(static), } ctx := context.Background() createUserParams := postgres.CreateUserParams{ Email: "test@example.com", Name: "test@example.com", Password: "this is my dumb password", } user, err := handler.Service.CreateUser(ctx, createUserParams) if err != nil { fmt.Println(err) t.Fail() return } budget, err := handler.Service.NewBudget(ctx, "My nice Budget", user.ID) if err != nil { fmt.Println(err) t.Fail() return } handler.DoYNABImport(ctx, t, budget) loc := time.Now().Location() AssertCategoriesAndAvailableEqual(ctx, t, loc, handler, budget) AssertAccountsEqual(ctx, t, handler, budget) } func AssertAccountsEqual(ctx context.Context, t *testing.T, handler *Handler, budget *postgres.Budget) { t.Helper() t.Run("account balances", func(t *testing.T) { t.Parallel() resultDir := "../testdata/production-export/results" files, err := os.ReadDir(resultDir) if err != nil { t.Errorf("could not load results: %s", err) return } for _, file := range files { if file.IsDir() { continue } name := file.Name()[0 : len(file.Name())-5] if name != "accounts" { continue } testCaseFile := filepath.Join(resultDir, file.Name()) handler.CheckAccountBalance(ctx, t, testCaseFile, budget) } }) } func (h Handler) CheckAccountBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget) { t.Helper() accounts, err := h.Service.GetAccountsWithBalance(ctx, budget.ID) if err != nil { t.Errorf("get accounts: %s", err) return } testDataFile, err := os.Open(testCaseFile) if err != nil { t.Errorf("could not load category test data: %s", err) return } var testData map[string]float64 dec := json.NewDecoder(testDataFile) err = dec.Decode(&testData) if err != nil { t.Errorf("could not decode category test data: %s", err) return } for accountName, accountBalance := range testData { found := false for _, account := range accounts { if account.Name == accountName { assertEqual(t, accountBalance, account.WorkingBalance.GetFloat64(), "balance for "+accountName) found = true } } if !found { t.Errorf("account " + accountName + " was not found in result") } } } func AssertCategoriesAndAvailableEqual(ctx context.Context, t *testing.T, loc *time.Location, handler *Handler, budget *postgres.Budget) { t.Helper() t.Run("Categories and available balance", func(t *testing.T) { t.Parallel() resultDir := "../testdata/production-export/results" files, err := os.ReadDir(resultDir) if err != nil { t.Errorf("could not load results: %s", err) return } for _, file := range files { if file.IsDir() { continue } name := file.Name()[0 : len(file.Name())-5] parts := strings.Split(name, "-") if len(parts) != 2 { continue } year, _ := strconv.Atoi(parts[0]) month, _ := strconv.Atoi(parts[1]) first := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, loc) testCaseFile := filepath.Join(resultDir, file.Name()) handler.CheckAvailableBalance(ctx, t, testCaseFile, budget, first) } }) } type TestData struct { AvailableBalance float64 Categories map[string]CategoryTestData } type CategoryTestData struct { Available float64 Activity float64 Assigned float64 } func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget, first time.Time) { t.Helper() t.Run(first.Format("2006-01"), func(t *testing.T) { t.Parallel() data, err := h.prepareBudgeting(ctx, *budget, first) if err != nil { t.Errorf("prepare budgeting: %s", err) return } testDataFile, err := os.Open(testCaseFile) if err != nil { t.Errorf("could not load category test data: %s", err) return } var testData TestData dec := json.NewDecoder(testDataFile) err = dec.Decode(&testData) if err != nil { t.Errorf("could not decode category test data: %s", err) return } assertEqual(t, testData.AvailableBalance, data.AvailableBalance.GetFloat64(), "available balance") for categoryName, categoryTestData := range testData.Categories { found := false for _, category := range data.Categories { name := category.Group + " : " + category.Name if name == categoryName { assertEqual(t, categoryTestData.Available, category.Available.GetFloat64(), "available for "+categoryName) assertEqual(t, categoryTestData.Activity, category.Activity.GetFloat64(), "activity for "+categoryName) assertEqual(t, categoryTestData.Assigned, category.Assigned.GetFloat64(), "assigned for "+categoryName) found = true } } if !found { t.Errorf("category " + categoryName + " was not found in result") } } }) } func assertEqual(t *testing.T, expected, actual float64, message string) { t.Helper() if expected == actual { return } t.Errorf("%s: expected %f, got %f", message, expected, actual) } func (h Handler) DoYNABImport(ctx context.Context, t *testing.T, budget *postgres.Budget) { t.Helper() budgetID := budget.ID ynab, err := postgres.NewYNABImport(ctx, h.Service.Queries, budgetID) if err != nil { fmt.Println(err) t.Fail() return } transactions, err := os.Open("../testdata/production-export/Register.tsv") if err != nil { fmt.Println(err) t.Fail() return } assignments, err := os.Open("../testdata/production-export/Budget.tsv") if err != nil { fmt.Println(err) t.Fail() return } err = ynab.ImportTransactions(ctx, transactions) if err != nil { fmt.Println(err) t.Fail() return } err = ynab.ImportAssignments(ctx, assignments) if err != nil { fmt.Println(err) t.Fail() return } }