github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/tests/basic_usage_test.go (about)

     1  package tests
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/stretchr/testify/require"
     8  
     9  	"github.com/ShoshinNikita/budget-manager/internal/db"
    10  	"github.com/ShoshinNikita/budget-manager/internal/pkg/money"
    11  	"github.com/ShoshinNikita/budget-manager/internal/web/api/models"
    12  )
    13  
    14  func TestBasicUsage(t *testing.T) {
    15  	t.Parallel()
    16  
    17  	RunTest(t, TestCases{
    18  		{Name: "spend types", Fn: testBasicUsage_SpendTypes},
    19  		{Name: "incomes", Fn: testBasicUsage_Incomes},
    20  		{Name: "monthly payments", Fn: testBasicUsage_MonthlyPayments},
    21  		{Name: "spends", Fn: testBasicUsage_Spends},
    22  		{Name: "search spends", Fn: testBasicUsage_SearchSpends},
    23  	})
    24  }
    25  
    26  func testBasicUsage_SpendTypes(t *testing.T, host string) {
    27  	require := require.New(t)
    28  
    29  	// Add
    30  	for i, req := range []RequestCreated{
    31  		{POST, SpendTypesPath, models.AddSpendTypeReq{Name: "f00d"}},                  // 1
    32  		{POST, SpendTypesPath, models.AddSpendTypeReq{Name: "fastfood", ParentID: 1}}, // 2
    33  		{POST, SpendTypesPath, models.AddSpendTypeReq{Name: "pizza", ParentID: 2}},    // 3
    34  		{POST, SpendTypesPath, models.AddSpendTypeReq{Name: "travel"}},                // 4
    35  		{POST, SpendTypesPath, models.AddSpendTypeReq{Name: "avia", ParentID: 4}},     // 5
    36  		{POST, SpendTypesPath, models.AddSpendTypeReq{Name: "house"}},                 // 6
    37  		{POST, SpendTypesPath, models.AddSpendTypeReq{Name: "entertainment"}},         // 7
    38  	} {
    39  		var resp models.AddSpendTypeResp
    40  		req.Send(t, host, &resp)
    41  		require.Equal(uint(i+1), resp.ID)
    42  	}
    43  
    44  	// Manage
    45  	for _, req := range []RequestOK{
    46  		{PUT, SpendTypesPath, models.EditSpendTypeReq{ID: 1, Name: ptrStr("food")}},
    47  		{PUT, SpendTypesPath, models.EditSpendTypeReq{ID: 3, ParentID: ptrUint(1)}},
    48  		{DELETE, SpendTypesPath, models.RemoveSpendTypeReq{ID: 5}},
    49  	} {
    50  		req.Send(t, host, nil)
    51  	}
    52  
    53  	// Check
    54  	var resp models.GetSpendTypesResp
    55  	RequestOK{GET, SpendTypesPath, nil}.Send(t, host, &resp)
    56  	require.Equal(
    57  		[]db.SpendType{
    58  			{ID: 1, Name: "food"},
    59  			{ID: 2, Name: "fastfood", ParentID: 1},
    60  			{ID: 3, Name: "pizza", ParentID: 1},
    61  			{ID: 4, Name: "travel"},
    62  			{ID: 6, Name: "house"},
    63  			{ID: 7, Name: "entertainment"},
    64  		},
    65  		resp.SpendTypes,
    66  	)
    67  }
    68  
    69  func testBasicUsage_Incomes(t *testing.T, host string) {
    70  	require := require.New(t)
    71  
    72  	// Add
    73  	for i, req := range []RequestCreated{
    74  		{POST, IncomesPath, models.AddIncomeReq{MonthID: 1, Title: "salary", Income: 2500}},                // 1
    75  		{POST, IncomesPath, models.AddIncomeReq{MonthID: 1, Title: "gifts", Income: 500}},                  // 2
    76  		{POST, IncomesPath, models.AddIncomeReq{MonthID: 1, Title: "temp", Income: 100}},                   // 3
    77  		{POST, IncomesPath, models.AddIncomeReq{MonthID: 1, Title: "cashback", Notes: "123", Income: 100}}, // 4
    78  	} {
    79  		var resp models.AddIncomeResp
    80  		req.Send(t, host, &resp)
    81  		require.Equal(uint(i+1), resp.ID)
    82  	}
    83  
    84  	// Manage
    85  	for _, req := range []RequestOK{
    86  		{PUT, IncomesPath, models.EditIncomeReq{ID: 2, Title: ptrStr("gift"), Notes: ptrStr("from friends")}},
    87  		{PUT, IncomesPath, models.EditIncomeReq{ID: 4, Income: ptrFloat(50)}},
    88  		{DELETE, IncomesPath, models.RemoveSpendTypeReq{ID: 3}},
    89  	} {
    90  		req.Send(t, host, nil)
    91  	}
    92  
    93  	// Check
    94  	month := getCurrentMonth(t, host)
    95  
    96  	expectedIncomes := []db.Income{
    97  		{ID: 1, Title: "salary", Income: money.FromInt(2500)},
    98  		{ID: 2, Title: "gift", Notes: "from friends", Income: money.FromInt(500)},
    99  		{ID: 4, Title: "cashback", Notes: "123", Income: money.FromInt(50)},
   100  	}
   101  	for i := range expectedIncomes {
   102  		expectedIncomes[i].Year = month.Year
   103  		expectedIncomes[i].Month = month.Month
   104  	}
   105  	require.Equal(expectedIncomes, month.Incomes)
   106  
   107  	checkMonth(require, 3050, 0, 0, month)
   108  }
   109  
   110  func testBasicUsage_MonthlyPayments(t *testing.T, host string) {
   111  	require := require.New(t)
   112  
   113  	// Add
   114  	for i, req := range []RequestCreated{
   115  		{POST, MonthlyPaymentsPath, models.AddMonthlyPaymentReq{MonthID: 1, Title: "rent", TypeID: 6, Cost: 800}},         // 1
   116  		{POST, MonthlyPaymentsPath, models.AddMonthlyPaymentReq{MonthID: 1, Title: "patre0n", Cost: 50}},                  // 2
   117  		{POST, MonthlyPaymentsPath, models.AddMonthlyPaymentReq{MonthID: 1, Title: "netflix", Notes: "remove", Cost: 20}}, // 3
   118  		{POST, MonthlyPaymentsPath, models.AddMonthlyPaymentReq{MonthID: 1, Title: "temp", Notes: "123", Cost: 100}},      // 4
   119  	} {
   120  		var resp models.AddMonthlyPaymentResp
   121  		req.Send(t, host, &resp)
   122  		require.Equal(uint(i+1), resp.ID)
   123  	}
   124  
   125  	// Manage
   126  	for _, req := range []RequestOK{
   127  		{PUT, MonthlyPaymentsPath, models.EditMonthlyPaymentReq{ID: 1, TypeID: ptrUint(0)}},
   128  		{PUT, MonthlyPaymentsPath, models.EditMonthlyPaymentReq{ID: 2, Title: ptrStr("patreon"), Notes: ptrStr("with VAT")}},
   129  		{PUT, MonthlyPaymentsPath, models.EditMonthlyPaymentReq{ID: 3, TypeID: ptrUint(7), Notes: ptrStr(""), Cost: ptrFloat(30)}},
   130  		{DELETE, MonthlyPaymentsPath, models.RemoveMonthlyPaymentReq{ID: 4}},
   131  	} {
   132  		req.Send(t, host, nil)
   133  	}
   134  
   135  	// Check
   136  	month := getCurrentMonth(t, host)
   137  
   138  	expectedMonthlyPayments := []db.MonthlyPayment{
   139  		{ID: 1, Title: "rent", Cost: money.FromInt(800)},
   140  		{ID: 2, Title: "patreon", Notes: "with VAT", Cost: money.FromInt(50)},
   141  		{ID: 3, Title: "netflix", Type: &db.SpendType{ID: 7, Name: "entertainment"}, Cost: money.FromInt(30)},
   142  	}
   143  	for i := range expectedMonthlyPayments {
   144  		expectedMonthlyPayments[i].Year = month.Year
   145  		expectedMonthlyPayments[i].Month = month.Month
   146  	}
   147  	require.Equal(expectedMonthlyPayments, month.MonthlyPayments)
   148  
   149  	checkMonth(require, 3050, -880, 0, month)
   150  }
   151  
   152  func testBasicUsage_Spends(t *testing.T, host string) {
   153  	require := require.New(t)
   154  
   155  	// Add
   156  	for i, req := range []RequestCreated{
   157  		{POST, SpendsPath, models.AddSpendReq{DayID: 1, Title: "bread", Notes: "fresh", TypeID: 1, Cost: 2}}, // 1
   158  		{POST, SpendsPath, models.AddSpendReq{DayID: 1, Title: "grocery", TypeID: 1, Cost: 10}},              // 2
   159  		{POST, SpendsPath, models.AddSpendReq{DayID: 1, Title: "milk", TypeID: 1, Cost: 2}},                  // 3
   160  		//
   161  		{POST, SpendsPath, models.AddSpendReq{DayID: 3, Title: "oil", TypeID: 1, Cost: 7}},            // 4
   162  		{POST, SpendsPath, models.AddSpendReq{DayID: 3, Title: "dinner in KFC", TypeID: 2, Cost: 15}}, // 5
   163  		//
   164  		{POST, SpendsPath, models.AddSpendReq{DayID: 10, Title: "bicycle", Notes: "https://example.com", Cost: 500}}, // 6
   165  		//
   166  		{POST, SpendsPath, models.AddSpendReq{DayID: 11, Title: "meat", TypeID: 1, Cost: 20}}, // 7
   167  		{POST, SpendsPath, models.AddSpendReq{DayID: 11, Title: "egg", TypeID: 1, Cost: 7}},   // 8
   168  		//
   169  		{POST, SpendsPath, models.AddSpendReq{DayID: 12, Title: "pizza", Cost: 100}}, // 9
   170  		//
   171  		{POST, SpendsPath, models.AddSpendReq{DayID: 15, Title: "book American Gods", Notes: "as a gift", Cost: 30}}, // 10
   172  		//
   173  		{POST, SpendsPath, models.AddSpendReq{DayID: 16, Title: "new mirror in the bathroom", TypeID: 6, Cost: 150}}, // 11
   174  		{POST, SpendsPath, models.AddSpendReq{DayID: 16, Title: "new towels", TypeID: 6, Cost: 50}},                  // 12
   175  		{POST, SpendsPath, models.AddSpendReq{DayID: 16, Title: "temp", Cost: 0}},                                    // 13
   176  	} {
   177  		var resp models.AddMonthlyPaymentResp
   178  		req.Send(t, host, &resp)
   179  		require.Equal(uint(i+1), resp.ID)
   180  	}
   181  
   182  	// Manage
   183  	for _, req := range []RequestOK{
   184  		{PUT, SpendsPath, models.EditSpendReq{ID: 4, TypeID: ptrUint(0)}},
   185  		{PUT, SpendsPath, models.EditSpendReq{ID: 8, Title: ptrStr("eggs"), Notes: ptrStr("10 count"), Cost: ptrFloat(8)}},
   186  		{PUT, SpendsPath, models.EditSpendReq{ID: 9, TypeID: ptrUint(3)}},
   187  		{DELETE, SpendsPath, models.RemoveSpendReq{ID: 13}},
   188  	} {
   189  		req.Send(t, host, nil)
   190  	}
   191  
   192  	// Check
   193  	month := getCurrentMonth(t, host)
   194  
   195  	expectedDays := []db.Day{
   196  		{ID: 1, Spends: []db.Spend{
   197  			{ID: 1, Title: "bread", Type: &db.SpendType{ID: 1, Name: "food"}, Notes: "fresh", Cost: money.FromInt(2)},
   198  			{ID: 2, Title: "grocery", Type: &db.SpendType{ID: 1, Name: "food"}, Cost: money.FromInt(10)},
   199  			{ID: 3, Title: "milk", Type: &db.SpendType{ID: 1, Name: "food"}, Cost: money.FromInt(2)},
   200  		}},
   201  		{ID: 2, Spends: []db.Spend{}},
   202  		{ID: 3, Spends: []db.Spend{
   203  			{ID: 4, Title: "oil", Cost: money.FromInt(7)},
   204  			{ID: 5, Title: "dinner in KFC", Type: &db.SpendType{ID: 2, Name: "fastfood", ParentID: 1}, Cost: money.FromInt(15)},
   205  		}},
   206  		{ID: 4, Spends: []db.Spend{}},
   207  		{ID: 5, Spends: []db.Spend{}},
   208  		{ID: 6, Spends: []db.Spend{}},
   209  		{ID: 7, Spends: []db.Spend{}},
   210  		{ID: 8, Spends: []db.Spend{}},
   211  		{ID: 9, Spends: []db.Spend{}},
   212  		{ID: 10, Spends: []db.Spend{
   213  			{ID: 6, Title: "bicycle", Notes: "https://example.com", Cost: money.FromInt(500)},
   214  		}},
   215  		{ID: 11, Spends: []db.Spend{
   216  			{ID: 7, Title: "meat", Type: &db.SpendType{ID: 1, Name: "food"}, Cost: money.FromInt(20)},
   217  			{ID: 8, Title: "eggs", Type: &db.SpendType{ID: 1, Name: "food"}, Notes: "10 count", Cost: money.FromInt(8)},
   218  		}},
   219  		{ID: 12, Spends: []db.Spend{
   220  			{ID: 9, Title: "pizza", Type: &db.SpendType{ID: 3, Name: "pizza", ParentID: 1}, Cost: money.FromInt(100)},
   221  		}},
   222  		{ID: 13, Spends: []db.Spend{}},
   223  		{ID: 14, Spends: []db.Spend{}},
   224  		{ID: 15, Spends: []db.Spend{
   225  			{ID: 10, Title: "book American Gods", Notes: "as a gift", Cost: money.FromInt(30)},
   226  		}},
   227  		{ID: 16, Spends: []db.Spend{
   228  			{ID: 11, Title: "new mirror in the bathroom", Type: &db.SpendType{ID: 6, Name: "house"}, Cost: money.FromInt(150)},
   229  			{ID: 12, Title: "new towels", Type: &db.SpendType{ID: 6, Name: "house"}, Cost: money.FromInt(50)},
   230  		}},
   231  	}
   232  	for i := len(expectedDays) + 1; i <= len(month.Days); i++ {
   233  		expectedDays = append(expectedDays, db.Day{ID: uint(i), Spends: []db.Spend{}})
   234  	}
   235  	for i := range expectedDays {
   236  		expectedDays[i].Year = month.Year
   237  		expectedDays[i].Month = month.Month
   238  		expectedDays[i].Day = int(expectedDays[i].ID)
   239  		for j := range expectedDays[i].Spends {
   240  			expectedDays[i].Spends[j].Year = expectedDays[i].Year
   241  			expectedDays[i].Spends[j].Month = expectedDays[i].Month
   242  			expectedDays[i].Spends[j].Day = expectedDays[i].Day
   243  		}
   244  
   245  		var prevSaldo money.Money
   246  		if i > 0 {
   247  			prevSaldo = expectedDays[i-1].Saldo
   248  		}
   249  		expectedDays[i].Saldo = prevSaldo.Add(month.DailyBudget)
   250  		for _, s := range expectedDays[i].Spends {
   251  			expectedDays[i].Saldo = expectedDays[i].Saldo.Sub(s.Cost)
   252  		}
   253  	}
   254  	require.Equal(expectedDays, month.Days)
   255  
   256  	checkMonth(require, 3050, -880, -894, month)
   257  }
   258  
   259  func testBasicUsage_SearchSpends(t *testing.T, host string) {
   260  	// Prepare spends
   261  	allSpends := []db.Spend{
   262  		{ID: 1, Day: 1, Title: "bread", Type: &db.SpendType{ID: 1, Name: "food"}, Notes: "fresh", Cost: money.FromInt(2)},
   263  		{ID: 2, Day: 1, Title: "grocery", Type: &db.SpendType{ID: 1, Name: "food"}, Cost: money.FromInt(10)},
   264  		{ID: 3, Day: 1, Title: "milk", Type: &db.SpendType{ID: 1, Name: "food"}, Cost: money.FromInt(2)},
   265  		{ID: 4, Day: 3, Title: "oil", Cost: money.FromInt(7)},
   266  		{ID: 5, Day: 3, Title: "dinner in KFC", Type: &db.SpendType{ID: 2, Name: "fastfood", ParentID: 1}, Cost: money.FromInt(15)},
   267  		{ID: 6, Day: 10, Title: "bicycle", Notes: "https://example.com", Cost: money.FromInt(500)},
   268  		{ID: 7, Day: 11, Title: "meat", Type: &db.SpendType{ID: 1, Name: "food"}, Cost: money.FromInt(20)},
   269  		{ID: 8, Day: 11, Title: "eggs", Type: &db.SpendType{ID: 1, Name: "food"}, Notes: "10 count", Cost: money.FromInt(8)},
   270  		{ID: 9, Day: 12, Title: "pizza", Type: &db.SpendType{ID: 3, Name: "pizza", ParentID: 1}, Cost: money.FromInt(100)},
   271  		{ID: 10, Day: 15, Title: "book American Gods", Notes: "as a gift", Cost: money.FromInt(30)},
   272  		{ID: 11, Day: 16, Title: "new mirror in the bathroom", Type: &db.SpendType{ID: 6, Name: "house"}, Cost: money.FromInt(150)},
   273  		{ID: 12, Day: 16, Title: "new towels", Type: &db.SpendType{ID: 6, Name: "house"}, Cost: money.FromInt(50)},
   274  	}
   275  	month := getCurrentMonth(t, host)
   276  	for i := range allSpends {
   277  		allSpends[i].Year = month.Year
   278  		allSpends[i].Month = month.Month
   279  	}
   280  
   281  	getSpends := func(ids ...uint) []db.Spend {
   282  		res := make([]db.Spend, 0, len(ids))
   283  		for _, id := range ids {
   284  			for _, spend := range allSpends {
   285  				if spend.ID == id {
   286  					res = append(res, spend)
   287  					break
   288  				}
   289  			}
   290  		}
   291  		return res
   292  	}
   293  	newDate := func(day int) time.Time {
   294  		return time.Date(month.Year, month.Month, day, 0, 0, 0, 0, time.UTC)
   295  	}
   296  
   297  	for _, tt := range []struct {
   298  		name string
   299  		req  models.SearchSpendsReq
   300  		ids  []uint
   301  	}{
   302  		{
   303  			name: "all spends",
   304  			req:  models.SearchSpendsReq{},
   305  			ids:  []uint{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
   306  		},
   307  		{
   308  			name: "filter by title",
   309  			req:  models.SearchSpendsReq{Title: "il"},
   310  			ids:  []uint{3, 4},
   311  		},
   312  		{
   313  			name: "filter by type",
   314  			req:  models.SearchSpendsReq{TypeIDs: []uint{1, 2}},
   315  			ids:  []uint{1, 2, 3, 5, 7, 8},
   316  		},
   317  		{
   318  			name: "filter by notes",
   319  			req:  models.SearchSpendsReq{Notes: "gift"},
   320  			ids:  []uint{10},
   321  		},
   322  		{
   323  			name: "filter by notes (exactly)",
   324  			req:  models.SearchSpendsReq{Notes: "gift", NotesExactly: true},
   325  			ids:  []uint{},
   326  		},
   327  		{
   328  			name: "filter by cost (min)",
   329  			req:  models.SearchSpendsReq{MinCost: 200},
   330  			ids:  []uint{6},
   331  		},
   332  		{
   333  			name: "filter by cost (max)",
   334  			req:  models.SearchSpendsReq{MaxCost: 7},
   335  			ids:  []uint{1, 3, 4},
   336  		},
   337  		{
   338  			name: "filter by cost (min and max)",
   339  			req:  models.SearchSpendsReq{MinCost: 10, MaxCost: 30},
   340  			ids:  []uint{2, 5, 7, 10},
   341  		},
   342  		{
   343  			name: "filter by time (after)",
   344  			req:  models.SearchSpendsReq{After: newDate(15)},
   345  			ids:  []uint{10, 11, 12},
   346  		},
   347  		{
   348  			name: "filter by time (before)",
   349  			req:  models.SearchSpendsReq{Before: newDate(2)},
   350  			ids:  []uint{1, 2, 3},
   351  		},
   352  		{
   353  			name: "filter by time (after and before)",
   354  			req:  models.SearchSpendsReq{After: newDate(2), Before: newDate(7)},
   355  			ids:  []uint{4, 5},
   356  		},
   357  		{
   358  			name: "sort by cost desc",
   359  			req:  models.SearchSpendsReq{MinCost: 7, MaxCost: 15, Sort: "cost", Order: "desc"},
   360  			ids:  []uint{5, 2, 8, 4},
   361  		},
   362  		{
   363  			name: "sort by title asc",
   364  			req:  models.SearchSpendsReq{TypeIDs: []uint{1}, Sort: "title"},
   365  			ids:  []uint{1, 8, 2, 7, 3},
   366  		},
   367  	} {
   368  		tt := tt
   369  		t.Run(tt.name, func(t *testing.T) {
   370  			require := require.New(t)
   371  
   372  			var resp models.SearchSpendsResp
   373  			RequestOK{GET, SearchSpendsPath, tt.req}.Send(t, host, &resp)
   374  			require.Equal(getSpends(tt.ids...), resp.Spends)
   375  		})
   376  	}
   377  }
   378  
   379  func getCurrentMonth(t *testing.T, host string) db.Month {
   380  	// It's very unlikely that tests were run at the last seconds of a month,
   381  	// and time.Now() will return the next month
   382  	year, month, _ := time.Now().Date()
   383  
   384  	var resp models.GetMonthResp
   385  	RequestOK{GET, MonthsPath, models.GetMonthByDateReq{Year: year, Month: month}}.Send(t, host, &resp)
   386  	return resp.Month
   387  }
   388  
   389  func checkMonth(require *require.Assertions, incomes, monthlyPayments, spends float64, month db.Month) {
   390  	inc := money.FromFloat(incomes)
   391  	require.Equal(inc, month.TotalIncome)
   392  
   393  	mp := money.FromFloat(monthlyPayments)
   394  	sp := money.FromFloat(spends)
   395  	total := mp.Add(sp)
   396  	require.Equal(total, month.TotalSpend)
   397  
   398  	res := inc.Add(total) // spends and monthlyPayments are < 0
   399  	require.Equal(res, month.Result)
   400  
   401  	dailyBudget := inc.Add(mp).Div(int64(len(month.Days)))
   402  	require.Equal(dailyBudget, month.DailyBudget)
   403  }