github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/internal/db/base/search_unit_test.go (about)

     1  package base
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/require"
    11  
    12  	common "github.com/ShoshinNikita/budget-manager/internal/db"
    13  	"github.com/ShoshinNikita/budget-manager/internal/pkg/money"
    14  )
    15  
    16  func TestBuildSearchSpendsQuery(t *testing.T) {
    17  	t.Parallel()
    18  
    19  	const defaultOrderByQuery = `ORDER BY month.year, month.month, day.day, spend.id`
    20  
    21  	buildWhereQuery := func(whereQuery string, orderByQuery string) string {
    22  		query := `
    23  			SELECT spend.id AS id, month.year AS year, month.month AS month, day.day AS day,
    24  			       spend.title AS title, spend.notes AS notes, spend.cost AS cost,
    25  			       spend_type.id AS "type.id", spend_type.name AS "type.name", spend_type.parent_id AS "type.parent_id"
    26  
    27  			 FROM spends AS spend
    28  			      INNER JOIN days AS day
    29  			      ON day.id = spend.day_id
    30  
    31  			      INNER JOIN months AS month
    32  			      ON month.id = day.month_id
    33  
    34  			      LEFT JOIN spend_types AS spend_type
    35  				  ON spend_type.id = spend.type_id`
    36  
    37  		query += "\n" + whereQuery + "\n"
    38  
    39  		query += orderByQuery
    40  
    41  		return formatQuery(query)
    42  	}
    43  
    44  	tests := []struct {
    45  		desc      string
    46  		args      common.SearchSpendsArgs
    47  		wantQuery string
    48  		wantArgs  []interface{}
    49  	}{
    50  		{
    51  			desc:      "no args",
    52  			wantQuery: buildWhereQuery("", defaultOrderByQuery), // empty WHERE clause
    53  			args:      common.SearchSpendsArgs{},
    54  		},
    55  		{
    56  			desc: "specify title",
    57  			args: common.SearchSpendsArgs{
    58  				Title: "rent",
    59  			},
    60  			wantQuery: buildWhereQuery(`WHERE LOWER(spend.title) LIKE ?`, defaultOrderByQuery),
    61  			wantArgs:  []interface{}{"%rent%"},
    62  		},
    63  		{
    64  			desc: "specify title (exactly)",
    65  			args: common.SearchSpendsArgs{
    66  				Title:        "rent",
    67  				TitleExactly: true,
    68  			},
    69  			wantQuery: buildWhereQuery(`WHERE LOWER(spend.title) LIKE ?`, defaultOrderByQuery),
    70  			wantArgs:  []interface{}{"rent"},
    71  		},
    72  		{
    73  			desc: "specify notes",
    74  			args: common.SearchSpendsArgs{
    75  				Notes: "note",
    76  			},
    77  			wantQuery: buildWhereQuery(`WHERE LOWER(spend.notes) LIKE ?`, defaultOrderByQuery),
    78  			wantArgs:  []interface{}{"%note%"},
    79  		},
    80  		{
    81  			desc: "specify notes (exactly)",
    82  			args: common.SearchSpendsArgs{
    83  				Notes:        "note",
    84  				NotesExactly: true,
    85  			},
    86  			wantQuery: buildWhereQuery(`WHERE LOWER(spend.notes) LIKE ?`, defaultOrderByQuery),
    87  			wantArgs:  []interface{}{"note"},
    88  		},
    89  		{
    90  			desc: "specify title and notes",
    91  			args: common.SearchSpendsArgs{
    92  				Title:        "rent",
    93  				TitleExactly: true,
    94  				Notes:        "note",
    95  			},
    96  			wantQuery: buildWhereQuery(
    97  				`WHERE LOWER(spend.title) LIKE ? AND LOWER(spend.notes) LIKE ?`,
    98  				defaultOrderByQuery,
    99  			),
   100  			wantArgs: []interface{}{"rent", "%note%"},
   101  		},
   102  		{
   103  			desc: "specify after",
   104  			args: common.SearchSpendsArgs{
   105  				After: time.Date(2018, time.January, 15, 15, 37, 0, 0, time.UTC),
   106  			},
   107  			wantQuery: buildWhereQuery(
   108  				`WHERE month.year*10000 + month.month*100 + day.day >= ?`,
   109  				defaultOrderByQuery,
   110  			),
   111  			wantArgs: []interface{}{20180115},
   112  		},
   113  		{
   114  			desc: "specify before",
   115  			args: common.SearchSpendsArgs{
   116  				Before: time.Date(2018, time.July, 28, 15, 37, 18, 0, time.UTC),
   117  			},
   118  			wantQuery: buildWhereQuery(
   119  				`WHERE month.year*10000 + month.month*100 + day.day <= ?`,
   120  				defaultOrderByQuery,
   121  			),
   122  			wantArgs: []interface{}{20180728},
   123  		},
   124  		{
   125  			desc: "specify after and before",
   126  			args: common.SearchSpendsArgs{
   127  				After:  time.Date(2018, time.January, 15, 15, 37, 0, 0, time.UTC),
   128  				Before: time.Date(2018, time.July, 28, 15, 37, 18, 0, time.UTC),
   129  			},
   130  			wantQuery: buildWhereQuery(
   131  				`WHERE month.year*10000 + month.month*100 + day.day BETWEEN ? AND ?`,
   132  				defaultOrderByQuery,
   133  			),
   134  			wantArgs: []interface{}{20180115, 20180728},
   135  		},
   136  		{
   137  			desc: "specify min cost",
   138  			args: common.SearchSpendsArgs{
   139  				MinCost: money.FromFloat(15.35),
   140  			},
   141  			wantQuery: buildWhereQuery(`WHERE spend.cost >= ?`, defaultOrderByQuery),
   142  			wantArgs:  []interface{}{1535},
   143  		},
   144  		{
   145  			desc: "specify max cost",
   146  			args: common.SearchSpendsArgs{
   147  				MaxCost: money.FromFloat(15000.50),
   148  			},
   149  			wantQuery: buildWhereQuery(`WHERE spend.cost <= ?`, defaultOrderByQuery),
   150  			wantArgs:  []interface{}{1500050},
   151  		},
   152  		{
   153  			desc: "specify min and max costs",
   154  			args: common.SearchSpendsArgs{
   155  				MinCost: money.FromFloat(15.35),
   156  				MaxCost: money.FromFloat(15000.50),
   157  			},
   158  			wantQuery: buildWhereQuery(`WHERE spend.cost BETWEEN ? AND ?`, defaultOrderByQuery),
   159  			wantArgs:  []interface{}{1535, 1500050},
   160  		},
   161  		{
   162  			desc: "specify type ids",
   163  			args: common.SearchSpendsArgs{
   164  				TypeIDs: []uint{1, 2, 5, 25, 3},
   165  			},
   166  			wantQuery: buildWhereQuery(`WHERE (spend.type_id IN (?,?,?,?,?))`, defaultOrderByQuery),
   167  			wantArgs:  []interface{}{1, 2, 5, 25, 3},
   168  		},
   169  		{
   170  			desc: "without type",
   171  			args: common.SearchSpendsArgs{
   172  				TypeIDs: []uint{0},
   173  			},
   174  			wantQuery: buildWhereQuery(`WHERE (spend.type_id IS NULL)`, defaultOrderByQuery),
   175  		},
   176  		{
   177  			desc: "with and without type",
   178  			args: common.SearchSpendsArgs{
   179  				TypeIDs: []uint{5, 3, 0},
   180  			},
   181  			wantQuery: buildWhereQuery(`WHERE (spend.type_id IS NULL OR spend.type_id IN (?,?))`, defaultOrderByQuery),
   182  			wantArgs:  []interface{}{5, 3},
   183  		},
   184  		{
   185  			desc: "all args",
   186  			args: common.SearchSpendsArgs{
   187  				Title:        "123",
   188  				Notes:        "some note",
   189  				NotesExactly: true,
   190  				After:        time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC),
   191  				Before:       time.Date(2020, time.February, 1, 0, 0, 0, 0, time.UTC),
   192  				MinCost:      money.FromFloat(200),
   193  				MaxCost:      money.FromFloat(5000),
   194  				TypeIDs:      []uint{0, 1, 7},
   195  			},
   196  			wantQuery: buildWhereQuery(`
   197  				WHERE LOWER(spend.title) LIKE ?
   198  					  AND LOWER(spend.notes) LIKE ?
   199  					  AND month.year*10000 + month.month*100 + day.day BETWEEN ? AND ?
   200  					  AND spend.cost BETWEEN ? AND ?
   201  					  AND (spend.type_id IS NULL OR spend.type_id IN (?,?))
   202  			`, defaultOrderByQuery),
   203  			wantArgs: []interface{}{
   204  				"%123%",
   205  				"some note",
   206  				20200101, 20200201,
   207  				20000, 500000,
   208  				1, 7,
   209  			},
   210  		},
   211  		{
   212  			desc: "sort by date (desc)",
   213  			args: common.SearchSpendsArgs{
   214  				Order: common.OrderByDesc,
   215  			},
   216  			wantQuery: buildWhereQuery(
   217  				"", `ORDER BY month.year DESC, month.month DESC, day.day DESC, spend.id`,
   218  			),
   219  		},
   220  		{
   221  			desc: "sort by title",
   222  			args: common.SearchSpendsArgs{
   223  				Sort: common.SortSpendsByTitle,
   224  			},
   225  			wantQuery: buildWhereQuery("", `ORDER BY spend.title, spend.id`),
   226  		},
   227  		{
   228  			desc: "sort by title (desc)",
   229  			args: common.SearchSpendsArgs{
   230  				Sort:  common.SortSpendsByTitle,
   231  				Order: common.OrderByDesc,
   232  			},
   233  			wantQuery: buildWhereQuery("", `ORDER BY spend.title DESC, spend.id`),
   234  		},
   235  		{
   236  			desc: "sort by cost",
   237  			args: common.SearchSpendsArgs{
   238  				Sort: common.SortSpendsByCost,
   239  			},
   240  			wantQuery: buildWhereQuery("", `ORDER BY spend.cost, spend.id`),
   241  		},
   242  		{
   243  			desc:      "sql injection",
   244  			wantQuery: buildWhereQuery(`WHERE LOWER(spend.title) LIKE ?`, defaultOrderByQuery),
   245  			args: common.SearchSpendsArgs{
   246  				Title:        "title'; OR 1=1--",
   247  				TitleExactly: true,
   248  			},
   249  			wantArgs: []interface{}{"title'; OR 1=1--"},
   250  		},
   251  	}
   252  	for _, tt := range tests {
   253  		tt := tt
   254  		t.Run(tt.desc, func(t *testing.T) {
   255  			query, args := (&DB{}).buildSearchSpendsQuery(tt.args)
   256  			require.Equal(t, tt.wantQuery, query)
   257  			require.Equal(t, tt.wantArgs, args)
   258  		})
   259  	}
   260  }
   261  
   262  func formatQuery(query string) string {
   263  	queryBuilder := strings.Builder{}
   264  
   265  	buff := bytes.NewBuffer([]byte(query))
   266  	scanner := bufio.NewScanner(buff)
   267  	for scanner.Scan() {
   268  		line := scanner.Bytes()
   269  
   270  		// Trim tabs, spaces and new line
   271  		line = bytes.TrimSpace(line)
   272  
   273  		if len(line) != 0 {
   274  			queryBuilder.Write(line)
   275  			queryBuilder.WriteByte(' ')
   276  		}
   277  	}
   278  	if scanner.Err() != nil {
   279  		panic(scanner.Err())
   280  	}
   281  
   282  	formattedQuery := queryBuilder.String()
   283  	formattedQuery = formattedQuery[:len(formattedQuery)-1] // trim the trailing space
   284  	return formattedQuery
   285  }