Bug 27993: Add unit tests
[koha.git] / t / db_dependent / TestBuilder.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Copyright 2014 - Biblibre SARL
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 13;
23 use Test::Warn;
24 use File::Basename qw(dirname);
25
26 use Koha::Database;
27 use Koha::Patrons;
28
29 BEGIN {
30     use_ok('t::lib::TestBuilder');
31 }
32
33 our $schema = Koha::Database->new->schema;
34 $schema->storage->txn_begin;
35 our $builder;
36
37 subtest 'Start with some trivial tests' => sub {
38     plan tests => 7;
39
40     $builder = t::lib::TestBuilder->new;
41     isnt( $builder, undef, 'We got a builder' );
42
43     my $data;
44     warning_like { $data = $builder->build; } qr/.+/, 'Catch a warning';
45     is( $data, undef, 'build without arguments returns undef' );
46     is( ref( $builder->schema ), 'Koha::Schema', 'check schema' );
47     is( ref( $builder->can('delete') ), 'CODE', 'found delete method' );
48
49     # invalid argument
50     warning_like { $builder->build({
51             source => 'Borrower',
52             value  => { surname => { invalid_hash => 1 } },
53         }) } qr/^Hash not allowed for surname/,
54         'Build should not accept a hash for this column';
55
56     # return undef if a record exists
57     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
58     my $param = { source => 'Branch', value => { branchcode => $branchcode } };
59     warning_like { $builder->build( $param ) }
60         qr/Violation of unique constraint/,
61         'Catch warn on adding existing record';
62 };
63
64
65 subtest 'Build all sources' => sub {
66     plan tests => 1;
67
68     my @sources = $builder->schema->sources;
69     my @source_in_failure;
70     for my $source ( @sources ) {
71         my $res;
72         # Skip the source if it is a view
73         next if $schema->source($source)->isa('DBIx::Class::ResultSource::View');
74         eval { $res = $builder->build( { source => $source } ); };
75         push @source_in_failure, $source if $@ || !defined( $res );
76     }
77     is( @source_in_failure, 0,
78         'TestBuilder should be able to create an object for every source' );
79     if ( @source_in_failure ) {
80         diag( "The following sources have not been generated correctly: " .
81         join ', ', @source_in_failure );
82     }
83 };
84
85
86 subtest 'Test length of some generated fields' => sub {
87     plan tests => 3;
88
89     # Test the length of a returned character field
90     my $bookseller = $builder->build({ source  => 'Aqbookseller' });
91     my $max = $schema->source('Aqbookseller')->column_info('phone')->{size};
92     is( length( $bookseller->{phone} ) > 0, 1,
93         'The length for a generated string (phone) should not be zero' );
94     is( length( $bookseller->{phone} ) <= $max, 1,
95         'Check maximum length for a generated string (phone)' );
96
97     my $item = $builder->build({ source => 'Item' });
98     is( $item->{replacementprice}, sprintf("%.2f", $item->{replacementprice}), "The number of decimals for floats should not be more than 2" );
99 };
100
101
102 subtest 'Test FKs in overduerules_transport_type' => sub {
103     plan tests => 5;
104
105     my $my_overduerules_transport_type = {
106         message_transport_type => {
107             message_transport_type => 'my msg_t_t',
108         },
109         overduerules_id => {
110             branchcode   => 'codeB',
111             categorycode => 'codeC',
112         },
113     };
114
115     my $overduerules_transport_type = $builder->build({
116         source => 'OverduerulesTransportType',
117         value  => $my_overduerules_transport_type,
118     });
119     is(
120         $overduerules_transport_type->{message_transport_type},
121         $my_overduerules_transport_type->{message_transport_type}->{message_transport_type},
122         'build stores the message_transport_type correctly'
123     );
124     is(
125         $schema->resultset('Overduerule')->find( $overduerules_transport_type->{overduerules_id} )->branchcode,
126         $my_overduerules_transport_type->{overduerules_id}->{branchcode},
127         'build stores the branchcode correctly'
128     );
129     is(
130         $schema->resultset('Overduerule')->find( $overduerules_transport_type->{overduerules_id} )->categorycode,
131         $my_overduerules_transport_type->{overduerules_id}->{categorycode},
132         'build stores the categorycode correctly'
133     );
134     is(
135         $schema->resultset('MessageTransportType')->find( $overduerules_transport_type->{message_transport_type} )->message_transport_type,
136         $overduerules_transport_type->{message_transport_type},
137         'build stores the foreign key message_transport_type correctly'
138     );
139     isnt(
140         $schema->resultset('Overduerule')->find( $my_overduerules_transport_type->{overduerules_id} )->letter2,
141         undef,
142         'build generates values if they are not given'
143     );
144 };
145
146
147 subtest 'Tests with composite FK in userpermission' => sub {
148     plan tests => 9;
149
150     my $my_user_permission = default_userpermission();
151     my $user_permission = $builder->build({
152         source => 'UserPermission',
153         value  => $my_user_permission,
154     });
155
156     # Checks on top level of userpermission
157     isnt(
158         $user_permission->{borrowernumber},
159         undef,
160         'build generates a borrowernumber correctly'
161     );
162     is(
163         $user_permission->{code},
164         $my_user_permission->{code}->{code},
165         'build stores code correctly'
166     );
167
168     # Checks one level deeper userpermission -> borrower
169     my $patron = $schema->resultset('Borrower')->find({ borrowernumber => $user_permission->{borrowernumber} });
170     is(
171         $patron->surname,
172         $my_user_permission->{borrowernumber}->{surname},
173         'build stores surname correctly'
174     );
175     isnt(
176         $patron->cardnumber,
177         undef,
178         'build generated cardnumber'
179     );
180
181     # Checks two levels deeper userpermission -> borrower -> branch
182     my $branch = $schema->resultset('Branch')->find({ branchcode => $patron->branchcode->branchcode });
183     is(
184         $branch->branchname,
185         $my_user_permission->{borrowernumber}->{branchcode}->{branchname},
186         'build stores branchname correctly'
187     );
188     isnt(
189         $branch->branchaddress1,
190         undef,
191         'build generated branch address'
192     );
193
194     # Checks with composite FK: userpermission -> permission
195     my $perm = $schema->resultset('Permission')->find({ module_bit => $user_permission->{module_bit}, code => $my_user_permission->{code}->{code} });
196     isnt( $perm, undef, 'build generated record for composite FK' );
197     is(
198         $perm->code,
199         $my_user_permission->{code}->{code},
200         'build stored code correctly'
201     );
202     is(
203         $perm->description,
204         $my_user_permission->{code}->{description},
205         'build stored description correctly'
206     );
207 };
208
209 sub default_userpermission {
210     return {
211         borrowernumber => {
212             surname => 'my surname',
213             address => 'my adress',
214             city    => 'my city',
215             branchcode => {
216                 branchname => 'my branchname',
217             },
218             categorycode => {
219                 hidelostitems   => 0,
220                 category_type   => 'A',
221                 default_privacy => 'default',
222             },
223             privacy => 1,
224         },
225         module_bit => {
226             flag        => 'my flag',
227         },
228         code => {
229             code        => 'my code',
230             description => 'my desc',
231         },
232     };
233 }
234
235
236 subtest 'Test build with NULL values' => sub {
237     plan tests => 3;
238
239     # PK should not be null
240     my $params = { source => 'Branch', value => { branchcode => undef }};
241     warning_like { $builder->build( $params ) }
242         qr/Null value for branchcode/,
243         'Catch warn on adding branch with a null branchcode';
244     # Nullable column
245     my $info = $schema->source( 'Item' )->column_info( 'barcode' );
246     $params = { source => 'Item', value  => { barcode => undef }};
247     my $item = $builder->build( $params );
248     is( $info->{is_nullable} && $item && !defined( $item->{barcode} ), 1,
249         'Barcode can be NULL' );
250     # Nullable FK
251     $params = { source => 'Reserve', value  => { itemnumber => undef }};
252     my $reserve = $builder->build( $params );
253     $info = $schema->source( 'Reserve' )->column_info( 'itemnumber' );
254     is( $reserve && $info->{is_nullable} && $info->{is_foreign_key} &&
255         !defined( $reserve->{itemnumber} ), 1, 'Nullable FK' );
256 };
257
258
259 subtest 'Tests for delete method' => sub {
260     plan tests => 12;
261
262     # Test delete with single and multiple records
263     my $basket1 = $builder->build({ source => 'Aqbasket' });
264     my $basket2 = $builder->build({ source => 'Aqbasket' });
265     my $basket3 = $builder->build({ source => 'Aqbasket' });
266     my ( $id1, $id2 ) = ( $basket1->{basketno}, $basket2->{basketno} );
267     $builder->delete({ source => 'Aqbasket', records => $basket1 });
268     isnt( exists $basket1->{basketno}, 1, 'Delete cleared PK hash value' );
269
270     is( $builder->schema->resultset('Aqbasket')->search({ basketno => $id1 })->count, 0, 'Basket1 is no longer found' );
271     is( $builder->schema->resultset('Aqbasket')->search({ basketno => $id2 })->count, 1, 'Basket2 is still found' );
272     is( $builder->delete({ source => 'Aqbasket', records => [ $basket2, $basket3 ] }), 2, "Returned two delete attempts" );
273     is( $builder->schema->resultset('Aqbasket')->search({ basketno => $id2 })->count, 0, 'Basket2 is no longer found' );
274
275
276     # Test delete in table without primary key (..)
277     is( $schema->source('TmpHoldsqueue')->primary_columns, 0,
278         'Table without primary key detected' );
279     my $bibno = 123; # just a number
280     my $cnt1 = $schema->resultset('TmpHoldsqueue')->count;
281     # Insert a new record in TmpHoldsqueue with that biblionumber
282     my $val = { biblionumber => $bibno };
283     my $rec = $builder->build({ source => 'TmpHoldsqueue', value => $val });
284     my $cnt2 = $schema->resultset('TmpHoldsqueue')->count;
285     is( defined($rec) && $cnt2 == $cnt1 + 1 , 1, 'Created a record' );
286     is( $builder->delete({ source => 'TmpHoldsqueue', records => $rec }),
287         undef, 'delete returns undef' );
288     is( $rec->{biblionumber}, $bibno, 'Hash value untouched' );
289     is( $schema->resultset('TmpHoldsqueue')->count, $cnt2,
290         "Method did not delete record in table without PK" );
291
292     # Test delete with NULL values
293     $val = { branchcode => undef };
294     is( $builder->delete({ source => 'Branch', records => $val }), 0,
295         'delete returns zero for an undef search with one key' );
296     $val = { module_bit => 1, #catalogue
297              code       => undef };
298     is( $builder->delete({ source => 'Permission', records => $val }), 0,
299         'delete returns zero for an undef search with a composite PK' );
300 };
301
302
303 subtest 'Auto-increment values tests' => sub {
304     plan tests => 3;
305
306     # Pick a table with AI PK
307     my $source  = 'Biblio'; # table
308     my $column  = 'biblionumber'; # ai column
309
310     my $col_info = $schema->source( $source )->column_info( $column );
311     is( $col_info->{is_auto_increment}, 1, "biblio.biblionumber is detected as autoincrement");
312
313     # Create a biblio
314     my $biblio_1 = $builder->build({ source => $source });
315     # Get the AI value
316     my $ai_value = $biblio_1->{ biblionumber };
317     # Create a biblio
318     my $biblio_2 = $builder->build({ source => $source });
319     # Get the next AI value
320     my $next_ai_value = $biblio_2->{ biblionumber };
321     is( $ai_value + 1, $next_ai_value, "AI values are consecutive");
322
323     # respect autoincr column
324     warning_like { $builder->build({
325             source => $source,
326             value  => { biblionumber => 123 },
327         }) } qr/^Value not allowed for auto_incr/,
328         'Build should not overwrite an auto_incr column';
329 };
330
331 subtest 'Date handling' => sub {
332     plan tests => 2;
333
334     $builder = t::lib::TestBuilder->new;
335
336     my $patron = $builder->build( { source => 'Borrower' } );
337     is( length( $patron->{updated_on} ),  19, 'A timestamp column value should be YYYY-MM-DD HH:MM:SS' );
338     is( length( $patron->{dateofbirth} ), 10, 'A date column value should be YYYY-MM-DD' );
339 };
340
341 subtest 'Default values' => sub {
342     plan tests => 3;
343
344     $builder = t::lib::TestBuilder->new;
345     my $item = $builder->build( { source => 'Item' } );
346     is( $item->{more_subfields_xml}, undef, 'This xml field should be undef' );
347     $item = $builder->build( { source => 'Item', value => { more_subfields_xml => 'some xml' } } );
348     is( $item->{more_subfields_xml}, 'some xml', 'Default should not overwrite assigned value' );
349
350     subtest 'generated dynamically (coderef)' => sub {
351         plan tests => 2;
352         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
353         like( $patron->category->category_type, qr{^(A|C|S|I|P|)$}, );
354
355         my $patron_category_X = $builder->build_object({ class => 'Koha::Patron::Categories', value => { category_type => 'X' } });
356         $patron = $builder->build_object({ class => 'Koha::Patrons', value => {categorycode => $patron_category_X->categorycode} });
357         is( $patron->category->category_type, 'X', );
358     };
359 };
360
361 subtest 'build_object() tests' => sub {
362
363     plan tests => 5;
364
365     $builder = t::lib::TestBuilder->new();
366
367     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
368     my $categorycode = $builder->build( { source => 'Category' } )->{categorycode};
369     my $itemtype = $builder->build( { source => 'Itemtype' } )->{itemtype};
370
371     my $issuing_rule = $builder->build_object(
372         {   class => 'Koha::CirculationRules',
373             value => {
374                 branchcode   => $branchcode,
375                 categorycode => $categorycode,
376                 itemtype     => $itemtype
377             }
378         }
379     );
380
381     is( ref($issuing_rule), 'Koha::CirculationRule', 'Type is correct' );
382     is( $issuing_rule->categorycode,
383         $categorycode, 'Category code correctly set' );
384     is( $issuing_rule->itemtype, $itemtype, 'Item type correctly set' );
385
386     subtest 'Test all classes' => sub {
387         my $Koha_modules_dir = dirname(__FILE__) . '/../../Koha';
388         my @koha_object_based_modules = `/bin/grep -rl -e '^sub object_class' $Koha_modules_dir`;
389         my @source_in_failure;
390         for my $module_filepath ( @koha_object_based_modules ) {
391             chomp $module_filepath;
392             next unless $module_filepath =~ m|\.pm$|;
393             my $module = $module_filepath;
394             $module =~ s|^.*/(Koha.*)\.pm$|$1|;
395             $module =~ s|/|::|g;
396             next if $module eq 'Koha::Objects';
397             eval "require $module";
398             my $object = $builder->build_object( { class => $module } );
399             is( ref($object), $module->object_class, "Testing $module" );
400             if ( ! grep {$module eq $_ } qw( Koha::Old::Patrons Koha::Statistics ) ) { # FIXME deletedborrowers and statistics do not have a PK
401                 eval {$object->get_from_storage};
402                 is( $@, '', "Module $module should have koha_object[s]_class method if needed" );
403             }
404
405             # Testing koha_object_class and koha_objects_class
406             my $object_class =  Koha::Object::_get_object_class($object->_result->result_class);
407             eval "require $object_class";
408             is( $@, '', "Module $object_class should be defined");
409             my $objects_class = Koha::Objects::_get_objects_class($object->_result->result_class);
410             eval "require $objects_class";
411             is( $@, '', "Module $objects_class should be defined");
412         }
413     };
414
415     subtest 'test parameters' => sub {
416         plan tests => 3;
417
418         warning_is { $issuing_rule = $builder->build_object( {} ); }
419         { carped => 'Missing class param' },
420             'The class parameter is mandatory, raises a warning if absent';
421         is( $issuing_rule, undef,
422             'If the class parameter is missing, undef is returned' );
423
424         warnings_like {
425             $builder->build_object(
426                 { class => 'Koha::Patrons', categorycode => 'foobar' } );
427         } qr{Unknown parameter\(s\): categorycode}, "Unknown parameter detected";
428     };
429 };
430
431 subtest '->build parameter' => sub {
432     plan tests => 4;
433
434     # Test to make sure build() warns user of unknown parameters.
435     warnings_are {
436         $builder->build({
437             source => 'Branch',
438             value => {
439                 branchcode => 'BRANCH_1'
440             }
441         })
442     } [], "No warnings on correct use";
443
444     warnings_like {
445         $builder->build({
446             source     => 'Branch',
447             branchcode => 'BRANCH_2' # This is wrong!
448         })
449     } qr/unknown param/i, "Carp unknown parameters";
450
451     warnings_like {
452         $builder->build({
453             zource     => 'Branch', # Intentional spelling error
454         })
455     } qr/Source parameter not specified/, "Catch warning on missing source";
456
457     warnings_like {
458         $builder->build(
459             { source => 'Borrower', categorycode => 'foobar' } );
460     } qr{Unknown parameter\(s\): categorycode}, "Unkown parameter detected";
461 };
462
463 $schema->storage->txn_rollback;