ParseFilter: Laundern schon vor dem eigentlichen parsen.
[kivitendo-erp.git] / t / controllers / helpers / parse_filter.t
1 use lib 't';
2
3 use Test::More tests => 36;
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   'type' => 'assembly',
247 }, {
248   query => [
249     'assembly' => 1
250   ],
251 }, 'object test without prefix', class => 'SL::DB::Manager::Part';
252
253 test {
254   'part.type' => 'assembly',
255 }, {
256   query => [
257     'part.assembly' => 1
258   ],
259 }, 'object test with prefix', class => 'SL::DB::Manager::OrderItem';
260
261 test {
262   'type' => [ 'part', 'assembly' ],
263 }, {
264   query => [
265     or => [
266      and => [ or => [ assembly => 0, assembly => undef ],
267               "!inventory_accno_id" => 0,
268               "!inventory_accno_id" => undef,
269      ],
270      assembly => 1,
271     ]
272   ],
273 }, 'object test without prefix but complex value', class => 'SL::DB::Manager::Part';
274
275 test {
276   'part.type' => [ 'part', 'assembly' ],
277 }, {
278   query => [
279     or => [
280      and => [ or => [ 'part.assembly' => 0, 'part.assembly' => undef ],
281               "!part.inventory_accno_id" => 0,
282               "!part.inventory_accno_id" => undef,
283      ],
284      'part.assembly' => 1,
285     ]
286   ],
287 }, 'object test with prefix but complex value', class => 'SL::DB::Manager::OrderItem';
288
289 test {
290   description => 'test'
291 }, {
292   query => [ description => 'test' ],
293   with_objects => [ 'order' ]
294 }, 'with_objects don\'t get clobbered', with_objects => [ 'order' ];
295
296 test {
297   customer => {
298     description => 'test'
299   }
300 }, {
301   query => [ 'customer.description' => 'test' ],
302   with_objects => [ 'order', 'customer' ]
303 }, 'with_objects get extended with auto infered objects', with_objects => [ 'order' ];
304
305 test {
306   customer => {
307     description => 'test'
308   }
309 }, {
310   query => [ 'customer.description' => 'test' ],
311   with_objects => [ 'order', 'customer' ]
312 }, 'with_objects get extended with auto infered objects with classes', class => 'SL::DB::Manager::Order',  with_objects => [ 'order' ];
313
314 test {
315   customer => {
316     description => 'test'
317   }
318 }, {
319   query => [ 'customer.description' => 'test' ],
320   with_objects => [ 'customer' ]
321 }, 'with_objects: no duplicates', with_objects => [ 'customer' ];
322
323 test {
324   part => {
325    'partnumber:substr::ilike' => '1',
326   },
327 }, {
328   query => [
329    'part.partnumber', {
330      ilike => '%1%'
331    }
332  ],
333  with_objects => [ 'part' ],
334 }, 'Regression check: prefixing of fallback filtering in relation with custom filters', class => 'SL::DB::Manager::OrderItem';
335 test {
336   'description:substr:multi::ilike' => 'term1 term2',
337 }, {
338   query => [
339     and => [
340       description => { ilike => '%term1%' },
341       description => { ilike => '%term2%' },
342     ]
343   ]
344 }, 'simple :multi';
345
346 test {
347   part => {
348     'all:substr:multi::ilike' => 'term1 term2',
349   },
350 }, {
351   query => [
352     and => [
353       or => [
354         'part.partnumber'  => { ilike => '%term1%' },
355         'part.description' => { ilike => '%term1%' },
356       ],
357       or => [
358         'part.partnumber'  => { ilike => '%term2%' },
359         'part.description' => { ilike => '%term2%' },
360       ],
361     ]
362   ],
363 }, 'complex :multi with custom dispatch and prefix', class => 'SL::DB::Manager::OrderItem';
364
365 test {
366   'description:substr:multi::ilike' => q|term1 "term2 and half" 'term3 and stuff'|,
367 }, {
368   query => [
369     and => [
370       description => { ilike => '%term1%' },
371       description => { ilike => '%term2 and half%' },
372       description => { ilike => '%term3 and stuff%' },
373     ]
374   ]
375 }, ':multi with complex tokenizing';
376
377 # test tokenizing for custom filters by monkeypatching a custom filter into Part
378 SL::DB::Manager::Part->add_filter_specs(
379   test => sub {
380     my ($key, $value, $prefix, @additional) = @_;
381     return "$prefix$key" => { @additional, $value };
382   }
383 );
384
385 test {
386   'part.test.what' => 2,
387 }, {
388   query => [
389     'part.test' => { 'what', 2 },
390   ]
391 }, 'additional tokens', class => 'SL::DB::Manager::OrderItem';
392
393 test {
394   'part.test.what:substr::ilike' => 2,
395 }, {
396   query => [
397     'part.test' => { 'what', { ilike => '%2%' } },
398   ]
399 }, 'additional tokens + filters + methods', class => 'SL::DB::Manager::OrderItem';