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 }