Merge branch 'b-3.6.1' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / t / controllers / helpers / parse_filter.t
1 use lib 't';
2
3 use Test::More tests => 41;
4 use Test::Deep;
5 use Data::Dumper;
6
7 use_ok 'Support::TestSetup';
8 use_ok 'SL::Controller::Helper::ParseFilter';
9
10 use SL::DB::OrderItem;
11
12 undef *::any; # Test::Deep exports any (for junctions) and MoreCommon exports any (like in List::Moreutils)
13
14 Support::TestSetup::login();
15 my ($filter, $expected);
16
17 sub test ($$$;%) {
18   my ($filter, $expect, $msg, %params) = @_;
19   my $target = delete $params{target};
20   my $args = { parse_filter($filter, %params) };
21   my $got  = $args; $target ||= '';
22      $got = $filter             if $target =~ /filter/;
23      $got = $params{launder_to} if $target =~ /launder/;
24   cmp_deeply(
25     $got,
26     $expect,
27     $msg,
28   ) or do {
29     print STDERR "expected => ", Dumper($expect), "\ngot: => ", Dumper($got), $/;
30   }
31 }
32
33 test { }, {
34 }, 'minimal test';
35
36 test {
37   name => 'Test',
38   whut => 'moof',
39 }, {
40   query => bag(
41     name => 'Test',
42     whut => 'moof'
43   ),
44 }, 'basic test';
45
46 test {
47   customer => {
48     name => 'rainer',
49   }
50 }, {
51   query => [ 'customer.name' => 'rainer' ],
52   with_objects => [ 'customer' ],
53 }, 'joining customers';
54
55 test {
56   customer => {
57     chart => {
58       accno => 'test',
59     }
60   }
61 }, {
62   query => [ 'customer.chart.accno' => 'test' ],
63   with_objects => bag( 'customer', 'customer.chart' ),
64 }, 'nested joins';
65
66 test {
67   'customer:substr' => 'Meyer'
68 }, {
69   query => [ customer => '%Meyer%' ]
70 }, 'simple filter substr';
71
72 test {
73   'customer::ilike' => 'Meyer'
74 }, {
75   query => [ customer => { ilike => 'Meyer' } ]
76 }, 'simple method ilike';
77
78 test {
79   customer => {
80     chart => {
81       'accno:tail::like' => '1200'
82     }
83   },
84 },
85 {
86   query => [ 'customer.chart.accno' => { like => '%1200' } ],
87   with_objects => bag('customer', 'customer.chart' ),
88 }, 'all together';
89
90
91 test {
92   customer => {
93     name => 'test',
94   },
95   invoice => {
96     customer => {
97       name => 'test',
98     },
99   },
100 }, {
101   'query' => bag(
102                'invoice.customer.name'  => 'test',
103                'customer.name'          => 'test',
104             ),
105   'with_objects' => bag( 'invoice.customer', 'customer', 'invoice' )
106 }, 'object in more than one relationship';
107
108 test {
109   'orddate:date::' => 'lt',
110   'orddate:date' => '20.3.2010',
111 }, {
112     'query' => [
113                  'orddate' => { 'lt' => isa('DateTime') }
114                ]
115
116 }, 'method dispatch and date constructor';
117
118 test {
119   id => [
120     123, 125, 157
121   ]
122 }, {
123   query => [ id => [ 123,125,157 ] ],
124 }, 'arrays as value';
125
126 test {
127   'sellprice:number' => [
128     '123,4', '2,34', '0,4',
129   ]
130 }, {
131   query => [
132     sellprice => [ 123.4, 2.34, 0.4 ],
133   ],
134 }, 'arrays with filter';
135
136
137 ########### laundering
138
139 test {
140   'sellprice:number' => [
141     '123,4', '2,34', '0,4',
142   ]
143 }, {
144   'sellprice:number' => [ '123,4', '2,34', '0,4' ],
145   'sellprice_number_' => { '123,4' => 1, '2,34' => 1, '0,4' => 1 },
146 }, 'laundering with array', target => 'filter';
147
148 my %args = (
149   'sellprice:number' => [
150     '123,4', '2,34', '0,4',
151   ],
152 );
153 test {
154   %args,
155 }, {
156   %args
157 }, 'laundering into launder does not alter filter', target => 'filter', launder_to => {};
158
159
160 test {
161   part => {
162    'sellprice:number' => '123,4',
163   }
164 }, {
165   part => {
166     'sellprice:number' => '123,4',
167     'sellprice_number' => '123,4'
168   }
169 }, 'deep laundering', target => 'filter';
170
171
172 test {
173   part => {
174    'sellprice:number' => '123,4',
175   }
176 }, {
177   part => {
178     'sellprice_number' => '123,4'
179   }
180 }, 'deep laundering, check for laundered hash', target => 'launder', launder_to => { };
181
182 test {
183   part => {
184    'sellprice:number' => '2',
185    'sellprice:number::' => 'le',
186   }
187 }, {
188   part => {
189    'sellprice:number' => '2',
190    'sellprice:number::' => 'le',
191   }
192 }, 'laundering of indirect filters does not alter', target => 'filter', launder_to => { };
193
194 test {
195   part => {
196    'sellprice:number' => '2',
197    'sellprice:number::' => 'le',
198   }
199 }, {
200   part => {
201     'sellprice_number' => '2',
202     'sellprice_number__' => 'le',
203   }
204 }, 'laundering of indirect filters', target => 'launder', launder_to => { };
205
206 test {
207   part => {
208    'sellprice:number' => '2',
209    'sellprice:number::' => 'le',
210   }
211 }, {
212   part => {
213     'sellprice:number' => '2',
214     'sellprice:number::' => 'le',
215     'sellprice_number' => '2',
216     'sellprice_number__' => 'le',
217   }
218 }, 'laundering of indirect filters - inplace', target => 'filter';
219
220 ### bug: sub objects
221
222 test {
223   order => {
224     customer => {
225       'name:substr::ilike' => 'test',
226     }
227   }
228 }, {
229   query => [ 'order.customer.name' => { ilike => '%test%' } ],
230   with_objects => bag('order.customer', 'order'),
231 }, 'sub objects have to retain their prefix';
232
233 ### class filter dispatch
234 #
235 test {
236   name => 'Test',
237   whut => 'moof',
238 }, {
239   query => bag(
240     name => 'Test',
241     whut => 'moof'
242   ),
243 }, 'object test simple', class => 'SL::DB::Manager::Part';
244
245 test {
246   'part_type' => 'assembly',
247 }, {
248   query => [
249              'part_type',
250              'assembly'
251            ] ,
252 }, 'object test without prefix', class => 'SL::DB::Manager::Part';
253
254 test {
255   'part.part_type' => 'assembly',
256 }, {
257   query => [
258              'part.part_type',
259              'assembly'
260            ]
261 }, 'object test with prefix', class => 'SL::DB::Manager::OrderItem';
262
263 test {
264   'part_type' => [ 'part', 'assembly' ],
265 }, {
266   query => [
267              'or',
268              [
269                'part_type',
270                'part',
271                'part_type',
272                'assembly'
273              ]
274            ]
275 }, 'object test without prefix but complex value', class => 'SL::DB::Manager::Part';
276 test {
277   'part.part_type' => [ 'part', 'assembly' ],
278 }, {
279   query => [
280              'or',
281              [
282                'part.part_type',
283                'part',
284                'part.part_type',
285                'assembly'
286              ]
287            ]
288 }, 'object test with prefix but complex value', class => 'SL::DB::Manager::OrderItem';
289
290 test {
291   description => 'test'
292 }, {
293   query => [ description => 'test' ],
294   with_objects => [ 'order' ]
295 }, 'with_objects don\'t get clobbered', with_objects => [ 'order' ];
296
297 test {
298   customer => {
299     description => 'test'
300   }
301 }, {
302   query => [ 'customer.description' => 'test' ],
303   with_objects => [ 'order', 'customer' ]
304 }, 'with_objects get extended with auto infered objects', with_objects => [ 'order' ];
305
306 test {
307   customer => {
308     description => 'test'
309   }
310 }, {
311   query => [ 'customer.description' => 'test' ],
312   with_objects => [ 'order', 'customer' ]
313 }, 'with_objects get extended with auto infered objects with classes', class => 'SL::DB::Manager::Order',  with_objects => [ 'order' ];
314
315 test {
316   customer => {
317     description => 'test'
318   }
319 }, {
320   query => [ 'customer.description' => 'test' ],
321   with_objects => [ 'customer' ]
322 }, 'with_objects: no duplicates', with_objects => [ 'customer' ];
323
324 test {
325   part => {
326    'partnumber:substr::ilike' => '1',
327   },
328 }, {
329   query => [
330    'part.partnumber', {
331      ilike => '%1%'
332    }
333  ],
334  with_objects => [ 'part' ],
335 }, 'Regression check: prefixing of fallback filtering in relation with custom filters', class => 'SL::DB::Manager::OrderItem';
336 test {
337   'description:substr:multi::ilike' => 'term1 term2',
338 }, {
339   query => [
340     and => [
341       description => { ilike => '%term1%' },
342       description => { ilike => '%term2%' },
343     ]
344   ]
345 }, 'simple :multi';
346
347 test {
348   part => {
349     'all:substr:multi::ilike' => 'term1 term2',
350   },
351 }, {
352   query => [
353     and => [
354       or => [
355         'part.partnumber'  => { ilike => '%term1%' },
356         'part.description' => { ilike => '%term1%' },
357         'part.ean'         => { ilike => '%term1%' },
358       ],
359       or => [
360         'part.partnumber'  => { ilike => '%term2%' },
361         'part.description' => { ilike => '%term2%' },
362         'part.ean'         => { ilike => '%term2%' },
363       ],
364     ]
365   ],
366 }, 'complex :multi with custom dispatch and prefix', class => 'SL::DB::Manager::OrderItem';
367
368 test {
369   'description:substr:multi::ilike' => q|term1 "term2 and half" 'term3 and stuff'|,
370 }, {
371   query => [
372     and => [
373       description => { ilike => '%term1%' },
374       description => { ilike => '%term2 and half%' },
375       description => { ilike => '%term3 and stuff%' },
376     ]
377   ]
378 }, ':multi with complex tokenizing';
379
380 # test tokenizing for custom filters by monkeypatching a custom filter into Part
381 SL::DB::Manager::Part->add_filter_specs(
382   test => sub {
383     my ($key, $value, $prefix, @additional) = @_;
384     return "$prefix$key" => { @additional, $value };
385   }
386 );
387
388 test {
389   'part.test.what' => 2,
390 }, {
391   query => [
392     'part.test' => { 'what', 2 },
393   ]
394 }, 'additional tokens', class => 'SL::DB::Manager::OrderItem';
395
396 test {
397   'part.test.what:substr::ilike' => 2,
398 }, {
399   query => [
400     'part.test' => { 'what', { ilike => '%2%' } },
401   ]
402 }, 'additional tokens + filters + methods', class => 'SL::DB::Manager::OrderItem';
403
404 test {
405   'orderitems.part.test.what:substr::ilike' => 2,
406 }, {
407   query => [
408     'orderitems.part.test' => { 'what', { ilike => '%2%' } },
409   ]
410 }, 'relationship + additional tokens + filters + methods', class => 'SL::DB::Manager::Order';
411
412 test {
413   part => {
414     'obsolete::lazy_bool_eq' => '0',
415   },
416 }, {
417   query => [
418       or => [
419         'part.obsolete' => undef,
420         'part.obsolete' => 0
421       ],
422   ],
423   with_objects => [ 'part' ],
424 }, 'complex methods modifying the key';
425
426
427 test {
428   'customer:substr::ilike' => ' Meyer'
429 }, {
430   query => [ customer => { ilike => '%Meyer%' } ]
431 }, 'auto trim 1';
432
433 test {
434   'customer:head::ilike' => ' Meyer '
435 }, {
436   query => [ customer => { ilike => 'Meyer%' } ]
437 }, 'auto trim 2';
438
439 test {
440   'customer:tail::ilike' => "\nMeyer\x{a0}"
441 }, {
442   query => [ customer => { ilike => '%Meyer' } ]
443 }, 'auto trim 2';