a91724ccecc8548ed01bdc60e321a1c9aad5b187
[kivitendo-erp.git] / t / db_helper / acts_as_list.t
1 use Test::More;
2 use Test::Exception;
3
4 use strict;
5
6 use lib 't';
7 use utf8;
8
9 use Carp;
10 use Data::Dumper;
11 use Support::TestSetup;
12 use SL::DB::TaxZone;
13
14 eval {
15   require SL::DB::RequirementSpec;
16   require SL::DB::RequirementSpecItem;
17   require SL::DB::RequirementSpecTextBlock;
18   require SL::DB::RequirementSpecType;
19   require SL::DB::RequirementSpecStatus;
20   1;
21 } or my $skip = 'RequirementSpec is not available for this test';
22
23 if ($skip) {
24   plan skip_all => $skip;
25 } else {
26   plan tests => 48;
27 }
28
29 my ($customer, $taxzone, $status, $type, $r_spec, @items);
30
31
32 sub init {
33   $taxzone  = SL::DB::Manager::TaxZone->find_by( description => 'Inland') || croak "No taxzone";
34   $customer = SL::DB::Customer->new(name => 'Test Customer', currency_id => $::instance_conf->get_currency_id, taxzone_id => $taxzone->id)->save;
35   $status   = SL::DB::Manager::RequirementSpecStatus->find_by(name => '', description => '') ||
36               SL::DB::RequirementSpecStatus->new(name => '', description => '', position => 0)->save;
37   $type     = SL::DB::Manager::RequirementSpecType->find_by(description => '') ||
38               SL::DB::RequirementSpecType->new(description => '', position => 0)->save;
39 }
40
41 sub cleanup {
42   $customer->delete;
43   $status->delete;
44   $type->delete;
45 }
46
47 sub cleanup_req_spec {
48   SL::DB::Manager::RequirementSpec->delete_all(where => [ customer_id => $customer->id ], cascade => 1);
49 }
50
51 sub reset_state {
52   cleanup_req_spec();
53   @items = ();
54
55   $r_spec = SL::DB::RequirementSpec->new(type_id => $type->id, status_id => $status->id, customer_id => $customer->id, hourly_rate => 42.24,  title => "Azumbo")->save;
56
57   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => undef,     position => 1, fb_number => "A01",    title => "Mühköh",   item_type => 'section',            description => "The Kuh.")->save;
58   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => undef,     position => 2, fb_number => "A02",    title => "Geheim",   item_type => 'section',            description => "Kofferkombination")->save;
59   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => get_id(1), position => 1, fb_number => "FB0001", title => "Yäääh",    item_type => 'function-block',     description => "Und so")->save;
60   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => get_id(1), position => 2, fb_number => "FB0012", title => "Blubb",    item_type => 'function-block',     description => "blabb")->save;
61   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => get_id(1), position => 3, fb_number => "FB0022", title => "Fingo",    item_type => 'function-block',     description => "fungo")->save;
62   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => get_id(4), position => 1, fb_number => "UFB002", title => "Suppi",    item_type => 'sub-function-block', description => "Suppa")->save;
63   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => get_id(4), position => 2, fb_number => "UFB000", title => "Suppa",    item_type => 'sub-function-block', description => "Suppi")->save;
64   push @items, SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, parent_id => get_id(2), position => 1, fb_number => "FB0018", title => "Neuneins", item_type => 'function-block',     description => "Eins")->save;
65
66 #  SL::DB::RequirementSpec->new->db->dbh->do(qq|SELECT pg_catalog.setval('| . $_->[0] . qq|', | . $_->[1] . qq|, true)|) for (['requirement_spec_items_id_seq', 8], [ 'requirement_specs_id_seq', 2 ]);
67 }
68
69 sub values_eq {
70   my ($val1, $val2) = @_;
71   return (defined($val1) == defined($val2))
72       && (!defined($val1) || ($val1 == $val2));
73 }
74
75 # accepts a list of arefs, with positions:
76 # 1 - pseudo id of item
77 # 2 - expceted pseudo parent_id
78 # 3 - expected position
79 sub test_positions {
80   my ($message, @positions) = @_;
81
82   my $failures =
83     join ' ',
84     map  { join ':', map { $_ // 'undef' } @{ $_ } }
85     grep { !values_eq($_->[1] && get_item($_->[1]) ? get_id($_->[1]) : undef, $_->[3]) || !values_eq($_->[2], $_->[4]) }
86     map  { my $item = SL::DB::RequirementSpecItem->new(id => get_id($_->[0]))->load; [ @{ $_ }, $item->parent_id, $item->position ] }
87     @positions;
88
89   is($failures, '', $message);
90 }
91
92 sub new_item {
93   my %params = @_;
94
95   my $new_item = SL::DB::RequirementSpecItem->new(requirement_spec_id => $r_spec->id, fb_number => 'dummy', title => 'dummy', item_type => $params{parent_id} ? 'function-block' : 'section', @_);
96   push @items, $new_item;
97   $new_item;
98 }
99
100 # the first version of this used hardcoded ids.
101 # to make things a little more portable the objects are now created with the
102 # default id sequences, but retain their numbering. whenever you see get_id or
103 # get_item used it looks up the old id in the @items array
104 sub get_item {
105   return SL::DB::RequirementSpecItem->new(id => get_id($_[0]))->load;
106 }
107
108 sub get_id {
109   $items[$_[0]-1]->id
110 }
111
112 Support::TestSetup::login();
113
114 my $item;
115
116 # 1
117 # `+- 3
118 #  +- 4
119 #  |  `+- 6
120 #  |   `- 7
121 #  `- 5
122 # 2
123 # `- 8
124
125 init();
126 reset_state();
127 test_positions "reset_state", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
128
129 # Einfügen neuer Objekte: "set_position"
130 new_item(parent_id => get_id(1))->save;
131 test_positions "set_position via new with parent_id NOT NULL", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ], [ 9, 1, 4 ];
132
133 reset_state();
134 new_item()->save;
135 test_positions "set_position via new with parent_id IS NULL",  [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ], [ 9, undef, 3 ];
136
137 # Löschen von Objekten: "remove_position"
138 reset_state();
139 get_item(3)->delete;
140 test_positions "remove_position via delete with parent_id NOT NULL",  [ 1, undef, 1 ], [ 2, undef, 2 ], [ 4, 1, 1 ], [ 5, 1, 2 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
141
142 reset_state();
143 get_item(1)->delete;
144 test_positions "remove_position via delete with parent_id IS NULL 1",  [ 2, undef, 1 ], [ 8, 2, 1 ];
145
146 reset_state();
147 get_item(2)->delete;
148 test_positions "remove_position via delete with parent_id IS NULL 2",  [ 1, undef, 1 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ];
149
150 # Hoch schieben
151 reset_state();
152 get_item(3)->move_position_up;
153 test_positions "move_position_up when at top of sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
154
155 reset_state();
156 get_item(4)->move_position_up;
157 test_positions "move_position_up when in middle of sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 4, 1, 1 ], [ 3, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
158
159 reset_state();
160 get_item(5)->move_position_up;
161 test_positions "move_position_up when at end of sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 5, 1, 2 ], [ 4, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
162
163 reset_state();
164 get_item(8)->move_position_up;
165 test_positions "move_position_up when only element in sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
166
167 # Herunter schieben
168 reset_state();
169 get_item(3)->move_position_down;
170 test_positions "move_position_down when at top of sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 4, 1, 1 ], [ 3, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
171
172 reset_state();
173 get_item(4)->move_position_down;
174 test_positions "move_position_down when in middle of sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 5, 1, 2 ], [ 4, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
175
176 reset_state();
177 get_item(5)->move_position_down;
178 test_positions "move_position_down when at bottom of sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
179
180 reset_state();
181 get_item(8)->move_position_down;
182 test_positions "move_position_down when only element in sub-list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
183
184 # Listen neu anordnen
185 reset_state();
186 get_item(8)->reorder_list(map { get_id($_) } 4, 5, 3);
187 test_positions "reorder_list called as instance method", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 4, 1, 1 ], [ 5, 1, 2 ], [ 3, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
188
189 reset_state();
190 SL::DB::RequirementSpecItem->reorder_list(map { get_id($_) } 4, 5, 3);
191 test_positions "reorder_list called as class method", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 4, 1, 1 ], [ 5, 1, 2 ], [ 3, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
192
193 # Aus Liste entfernen
194 reset_state();
195 get_item(3)->remove_from_list;
196 test_positions "remove_from_list on top", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, -1 ], [ 4, 1, 1 ], [ 5, 1, 2 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
197
198 reset_state();
199 get_item(4)->remove_from_list;
200 test_positions "remove_from_list on middle", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, -1 ], [ 5, 1, 2 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
201
202 reset_state();
203 get_item(5)->remove_from_list;
204 test_positions "remove_from_list on bottom", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, -1 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
205
206 reset_state();
207 get_item(8)->remove_from_list;
208 test_positions "remove_from_list on only item in list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, -1 ];
209
210 reset_state();
211 $item = get_item(3); $item->remove_from_list; $item->delete;
212 test_positions "remove_from_list and delete afterwards", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 4, 1, 1 ], [ 5, 1, 2 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
213
214 # Zu Liste hinzufügen
215 reset_state();
216 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'last');
217 test_positions "add_to_list position 'last'", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 4 ];
218
219 reset_state();
220 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'first');
221 test_positions "add_to_list position 'first'", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 2 ], [ 4, 1, 3 ], [ 5, 1, 4 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 1 ];
222
223 reset_state();
224 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'before', reference => get_id(3));
225 test_positions "add_to_list position 'before' first by ID", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 2 ], [ 4, 1, 3 ], [ 5, 1, 4 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 1 ];
226
227 reset_state();
228 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'before', reference => get_item(3));
229 test_positions "add_to_list position 'before' first by reference", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 2 ], [ 4, 1, 3 ], [ 5, 1, 4 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 1 ];
230
231 reset_state();
232 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'before', reference => get_id(4));
233 test_positions "add_to_list position 'before' middle by ID", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 3 ], [ 5, 1, 4 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 2 ];
234
235 reset_state();
236 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'before', reference => get_item(4));
237 test_positions "add_to_list position 'before' middle by reference", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 3 ], [ 5, 1, 4 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 2 ];
238
239 reset_state();
240 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'after', reference => get_id(5));
241 test_positions "add_to_list position 'after' last by ID", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 4 ];
242
243 reset_state();
244 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'after', reference => get_item(5));
245 test_positions "add_to_list position 'after' last by reference", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 4 ];
246
247 reset_state();
248 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'after', reference => get_id(4));
249 test_positions "add_to_list position 'after' middle by ID", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 4 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 3 ];
250
251 reset_state();
252 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(1)); $item->add_to_list(position => 'after', reference => get_item(4));
253 test_positions "add_to_list position 'after' middle by reference", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 4 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 1, 3 ];
254
255 reset_state();
256 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(3)); $item->add_to_list(position => 'last');
257 test_positions "add_to_list position 'last' in empty", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 3, 1 ];
258
259 reset_state();
260 $item = get_item(8); $item->remove_from_list; $item->parent_id(get_id(3)); $item->add_to_list(position => 'first');
261 test_positions "add_to_list position 'first' in empty", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 4, 1, 2 ], [ 5, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 3, 1 ];
262
263 reset_state();
264 $item = get_item(5); $item->add_to_list(position => 'after', reference => get_id(3));
265 test_positions "add_to_list without prior remove_from_list", [ 1, undef, 1 ], [ 2, undef, 2 ], [ 3, 1, 1 ], [ 5, 1, 2 ], [ 4, 1, 3 ], [ 6, 4, 1 ], [ 7, 4, 2 ], [ 8, 2, 1 ];
266
267 reset_state();
268 $item = get_item(4);
269 is($item->get_next_in_list->id,                       get_id(5), 'Next of 4 is 5');
270 is($item->get_previous_in_list->id,                   get_id(3), 'Previous of 4 is 5');
271 is($item->get_next_in_list->get_previous_in_list->id, get_id(4), 'Previous of Next of 4 is 4');
272 is($item->get_previous_in_list->get_next_in_list->id, get_id(4), 'Next of Previous of 4 is 4');
273 is($item->get_next_in_list->get_next_in_list,         undef,     'Next of Next of 4 is undef');
274 is($item->get_previous_in_list->get_previous_in_list, undef,     'Previous of Previous of 4 is undef');
275
276 # Parametervalidierung
277 throws_ok { new_item()->move_position_up   } qr/not.*been.*saved/i, 'move up not saved yet';
278 throws_ok { new_item()->move_position_down } qr/not.*been.*saved/i, 'move down not saved yet';
279
280 throws_ok { $item = get_item(8); $item->position(undef); $item->move_position_up   } qr/no.*position.*set.*yet/i, 'move up no position set';
281 throws_ok { $item = get_item(8); $item->position(undef); $item->move_position_down } qr/no.*position.*set.*yet/i, 'move down no position set';
282
283 throws_ok { get_item(8)->add_to_list;                      } qr/invalid.*parameter.*position/i,  'missing position';
284 throws_ok { get_item(8)->add_to_list(position => 'gonzo')  } qr/invalid.*parameter.*position/i,  'invalid position';
285 throws_ok { get_item(8)->add_to_list(position => 'before') } qr/missing.*parameter.*reference/i, 'missing reference for position "before"';
286 throws_ok { get_item(8)->add_to_list(position => 'after')  } qr/missing.*parameter.*reference/i, 'missing reference for position "after"';
287
288 END {
289   # safely try to clean up
290   eval {
291     cleanup_req_spec();
292     cleanup();
293   }
294 }