updated DB-dependent tests to reflect labels rewrite
[koha.git] / t / lib / KohaTest.pm
1 package KohaTest;
2 use base qw(Test::Class);
3
4 use Test::More;
5 use Data::Dumper;
6
7 eval "use Test::Class";
8 plan skip_all => "Test::Class required for performing database tests" if $@;
9 # Or, maybe I should just die there.
10
11 use C4::Auth;
12 use C4::Biblio;
13 use C4::Bookfund;
14 use C4::Bookseller;
15 use C4::Context;
16 use C4::Items;
17 use C4::Members;
18 use C4::Search;
19 use C4::Installer;
20 use C4::Languages;
21 use File::Temp qw/ tempdir /;
22 use CGI;
23 use Time::localtime;
24
25 # Since this is an abstract base class, this prevents these tests from
26 # being run directly unless we're testing a subclass. It just makes
27 # things faster.
28 __PACKAGE__->SKIP_CLASS( 1 );
29
30 INIT {
31     if ($ENV{SINGLE_TEST}) {
32         # if we're running the tests in one
33         # or more test files specified via
34         #
35         #   make test-single TEST_FILES=lib/KohaTest/Foo.pm
36         #
37         # use this INIT trick taken from the POD for
38         # Test::Class::Load.
39         start_zebrasrv();
40         Test::Class->runtests;
41         stop_zebrasrv();
42     }
43 }
44
45 use Attribute::Handlers;
46
47 =head2 Expensive test method attribute
48
49 If a test method is decorated with an Expensive
50 attribute, it is skipped unless the RUN_EXPENSIVE_TESTS
51 environment variable is defined.
52
53 To declare an entire test class and its subclasses expensive,
54 define a SKIP_CLASS with the Expensive attribute:
55
56     sub SKIP_CLASS : Expensive { }
57
58 =cut
59
60 sub Expensive : ATTR(CODE) {
61     my ($package, $symbol, $sub, $attr, $data, $phase) = @_;
62     my $name = *{$symbol}{NAME};
63     if ($name eq 'SKIP_CLASS') {
64         if ($ENV{'RUN_EXPENSIVE_TESTS'}) {
65             *{$symbol} = sub { 0; }
66         } else {
67             *{$symbol} = sub { "Skipping expensive test classes $package (and subclasses)"; }
68         }
69     } else {
70         unless ($ENV{'RUN_EXPENSIVE_TESTS'}) {
71             # a test method that runs no tests and just returns a scalar is viewed by Test::Class as a skip
72             *{$symbol} = sub { "Skipping expensive test $package\:\:$name"; }
73         }
74     }
75 }
76
77 =head2 startup methods
78
79 these are run once, at the beginning of the whole test suite
80
81 =cut
82
83 sub startup_15_truncate_tables : Test( startup => 1 ) {
84     my $self = shift;
85     
86     my @truncate_tables = qw( accountlines 
87                               accountoffsets              
88                               alert                       
89                               aqbasket                    
90                               aqbooksellers               
91                               aqorderbreakdown            
92                               aqorderdelivery             
93                               aqorders                    
94                               auth_header                 
95                               branchcategories            
96                               branchrelations             
97                               branchtransfers             
98                               browser                     
99                               cities                      
100                               deletedbiblio               
101                               deletedbiblioitems          
102                               deletedborrowers            
103                               deleteditems                
104                               ethnicity                   
105                               issues                      
106                               issuingrules                
107                               labels_batches
108                               labels_layouts
109                               labels_templates
110                               matchchecks                 
111                               notifys                     
112                               nozebra                     
113                               old_issues                  
114                               old_reserves                
115                               overduerules                
116                               patroncards                 
117                               patronimage                 
118                               printers                    
119                               printers_profile            
120                               reports_dictionary          
121                               reserveconstraints          
122                               reserves                    
123                               reviews                     
124                               roadtype                    
125                               saved_reports               
126                               saved_sql                   
127                               serial                      
128                               serialitems                 
129                               services_throttle           
130                               special_holidays            
131                               statistics                  
132                               subscription                
133                               subscriptionhistory         
134                               subscriptionroutinglist     
135                               suggestions                 
136                               tags                        
137                               virtualshelfcontents        
138                         );
139     
140     my $failed_to_truncate = 0;
141     foreach my $table ( @truncate_tables ) {
142         my $dbh = C4::Context->dbh();
143         $dbh->do( "truncate $table" )
144           or $failed_to_truncate = 1;
145     }
146     is( $failed_to_truncate, 0, 'truncated tables' );
147 }
148
149 =head2 startup_20_add_bookseller
150
151 we need a bookseller for many of the tests, so let's insert one. Feel
152 free to use this one, or insert your own.
153
154 =cut
155
156 sub startup_20_add_bookseller : Test(startup => 1) {
157     my $self = shift;
158
159     my $booksellerinfo = { name => 'bookseller ' . $self->random_string(),
160                       };
161
162     my $id = AddBookseller( $booksellerinfo );
163     ok( $id, "created bookseller: $id" );
164     $self->{'booksellerid'} = $id;
165     
166     return;
167 }
168
169 =head2 startup_22_add_bookfund
170
171 we need a bookfund for many of the tests. This currently uses one that
172 is in the skeleton database.  free to use this one, or insert your
173 own.
174
175 =cut
176
177 sub startup_22_add_bookfund : Test(startup => 2) {
178     my $self = shift;
179
180     my $bookfundid = 'GEN';
181     my $bookfund = GetBookFund( $bookfundid, undef );
182     # diag( Data::Dumper->Dump( [ $bookfund ], qw( bookfund  ) ) );
183     is( $bookfund->{'bookfundid'},   $bookfundid,      "found bookfund: '$bookfundid'" );
184     is( $bookfund->{'bookfundname'}, 'General Stacks', "found bookfund: '$bookfundid'" );
185     
186     $self->{'bookfundid'} = $bookfundid;
187     return;
188 }
189
190 =head2 startup_24_add_branch
191
192 =cut
193
194 sub startup_24_add_branch : Test(startup => 1) {
195     my $self = shift;
196
197     my $branch_info = {
198         add            => 1,
199         branchcode     => $self->random_string(3),
200         branchname     => $self->random_string(),
201         branchaddress1 => $self->random_string(),
202         branchaddress2 => $self->random_string(),
203         branchaddress3 => $self->random_string(),
204         branchphone    => $self->random_phone(),
205         branchfax      => $self->random_phone(),
206         brancemail     => $self->random_email(),
207         branchip       => $self->random_ip(),
208         branchprinter  => $self->random_string(),
209       };
210     C4::Branch::ModBranch($branch_info);
211     $self->{'branchcode'} = $branch_info->{'branchcode'};
212     ok( $self->{'branchcode'}, "created branch: $self->{'branchcode'}" );
213
214 }
215
216 =head2 startup_24_add_member
217
218 Add a patron/member for the tests to use
219
220 =cut
221
222 sub startup_24_add_member : Test(startup => 1) {
223     my $self = shift;
224
225     my $memberinfo = { surname      => 'surname '  . $self->random_string(),
226                        firstname    => 'firstname' . $self->random_string(),
227                        address      => 'address'   . $self->random_string(),
228                        city         => 'city'      . $self->random_string(),
229                        cardnumber   => 'card'      . $self->random_string(),
230                        branchcode   => 'CPL', # CPL => Centerville
231                        categorycode => 'PT',  # PT  => PaTron
232                        dateexpiry   => '2010-01-01',
233                        password     => 'testpassword',
234                        dateofbirth  => $self->random_date(),
235                   };
236
237     my $borrowernumber = AddMember( %$memberinfo );
238     ok( $borrowernumber, "created member: $borrowernumber" );
239     $self->{'memberid'} = $borrowernumber;
240     
241     return;
242 }
243
244 =head2 startup_30_login
245
246 =cut
247
248 sub startup_30_login : Test( startup => 2 ) {
249     my $self = shift;
250
251     $self->{'sessionid'} = '12345678'; # does this value matter?
252     my $borrower_details = C4::Members::GetMemberDetails( $self->{'memberid'} );
253     ok( $borrower_details->{'cardnumber'}, 'cardnumber' );
254     
255     # make a cookie and force it into $cgi.
256     # This would be a lot easier with Test::MockObject::Extends.
257     my $cgi = CGI->new( { userid   => $borrower_details->{'cardnumber'},
258                           password => 'testpassword' } );
259     my $setcookie = $cgi->cookie( -name  => 'CGISESSID',
260                                   -value => $self->{'sessionid'} );
261     $cgi->{'.cookies'} = { CGISESSID => $setcookie };
262     is( $cgi->cookie('CGISESSID'), $self->{'sessionid'}, 'the CGISESSID cookie is set' );
263     # diag( Data::Dumper->Dump( [ $cgi->cookie('CGISESSID') ], [ qw( cookie ) ] ) );
264
265     # C4::Auth::checkauth sometimes emits a warning about unable to append to sessionlog. That's OK.
266     my ( $userid, $cookie, $sessionID ) = C4::Auth::checkauth( $cgi, 'noauth', {}, 'intranet' );
267     # diag( Data::Dumper->Dump( [ $userid, $cookie, $sessionID ], [ qw( userid cookie sessionID ) ] ) );
268
269     # my $session = C4::Auth::get_session( $sessionID );
270     # diag( Data::Dumper->Dump( [ $session ], [ qw( session ) ] ) );
271     
272
273 }
274
275 =head2 setup methods
276
277 setup methods are run before every test method
278
279 =cut
280
281 =head2 teardown methods
282
283 teardown methods are many time, once at the end of each test method.
284
285 =cut
286
287 =head2 shutdown methods
288
289 shutdown methods are run once, at the end of the test suite
290
291 =cut
292
293 =head2 utility methods
294
295 These are not test methods, but they're handy
296
297 =cut
298
299 =head3 random_string
300
301 Nice for generating names and such. It's not actually random, more
302 like arbitrary.
303
304 =cut
305
306 sub random_string {
307     my $self = shift;
308
309     my $wordsize = shift || 6;  # how many letters in your string?
310
311     # leave out these characters: "oOlL10". They're too confusing.
312     my @alphabet = ( 'a'..'k','m','n','p'..'z', 'A'..'K','M','N','P'..'Z', 2..9 );
313
314     my $randomstring;
315     foreach ( 0..$wordsize ) {
316         $randomstring .= $alphabet[ rand( scalar( @alphabet ) ) ];
317     }
318     return $randomstring;
319     
320 }
321
322 =head3 random_phone
323
324 generates a random phone number. Currently, it's not actually random. It's an unusable US phone number
325
326 =cut
327
328 sub random_phone {
329     my $self = shift;
330
331     return '212-555-5555';
332     
333 }
334
335 =head3 random_email
336
337 generates a random email address. They're all in the unusable
338 'example.com' domain that is designed for this purpose.
339
340 =cut
341
342 sub random_email {
343     my $self = shift;
344
345     return $self->random_string() . '@example.com';
346     
347 }
348
349 =head3 random_ip
350
351 returns an IP address suitable for testing purposes.
352
353 =cut
354
355 sub random_ip {
356     my $self = shift;
357
358     return '127.0.0.2';
359     
360 }
361
362 =head3 random_date
363
364 returns a somewhat random date in the iso (yyyy-mm-dd) format.
365
366 =cut
367
368 sub random_date {
369     my $self = shift;
370
371     my $year  = 1800 + int( rand(300) );    # 1800 - 2199
372     my $month = 1 + int( rand(12) );        # 1 - 12
373     my $day   = 1 + int( rand(28) );        # 1 - 28
374                                             # stop at the 28th to keep us from generating February 31st and such.
375
376     return sprintf( '%04d-%02d-%02d', $year, $month, $day );
377
378 }
379
380 =head3 tomorrow
381
382 returns tomorrow's date as YYYY-MM-DD.
383
384 =cut
385
386 sub tomorrow {
387     my $self = shift;
388
389     return $self->days_from_now( 1 );
390
391 }
392
393 =head3 yesterday
394
395 returns yesterday's date as YYYY-MM-DD.
396
397 =cut
398
399 sub yesterday {
400     my $self = shift;
401
402     return $self->days_from_now( -1 );
403 }
404
405
406 =head3 days_from_now
407
408 returns an arbitrary date based on today in YYYY-MM-DD format.
409
410 =cut
411
412 sub days_from_now {
413     my $self = shift;
414     my $days = shift or return;
415
416     my $seconds = time + $days * 60*60*24;
417     my $yyyymmdd = sprintf( '%04d-%02d-%02d',
418                             localtime( $seconds )->year() + 1900,
419                             localtime( $seconds )->mon() + 1,
420                             localtime( $seconds )->mday() );
421     return $yyyymmdd;
422 }
423
424 =head3 add_biblios
425
426   $self->add_biblios( count     => 10,
427                       add_items => 1, );
428
429   named parameters:
430      count: number of biblios to add
431      add_items: should you add items for each one?
432
433   returns:
434     I don't know yet.
435
436   side effects:
437     adds the biblionumbers to the $self->{'biblios'} listref
438
439   Notes:
440     Should I allow you to pass in biblio information, like title?
441     Since this method is in the KohaTest class, all tests in it will be ignored, unless you call this from your own namespace.
442     This runs 10 tests, plus 4 for each "count", plus 3 more for each item added.
443
444 =cut
445
446 sub add_biblios {
447     my $self = shift;
448     my %param = @_;
449
450     $param{'count'}     = 1 unless defined( $param{'count'} );
451     $param{'add_items'} = 0 unless defined( $param{'add_items'} );
452
453     foreach my $counter ( 1..$param{'count'} ) {
454         my $marcrecord  = MARC::Record->new();
455         isa_ok( $marcrecord, 'MARC::Record' );
456         my @marc_fields = ( MARC::Field->new( '100', '1', '0',
457                                               a => 'Twain, Mark',
458                                               d => "1835-1910." ),
459                             MARC::Field->new( '245', '1', '4',
460                                               a => sprintf( 'The Adventures of Huckleberry Finn Test %s', $counter ),
461                                               c => "Mark Twain ; illustrated by E.W. Kemble." ),
462                             MARC::Field->new( '952', '0', '0',
463                                               p => '12345678' . $self->random_string() ),   # barcode
464                             MARC::Field->new( '952', '0', '0',
465                                               o => $self->random_string() ),   # callnumber
466                             MARC::Field->new( '952', '0', '0',
467                                               a => 'CPL',
468                                               b => 'CPL' ),
469                        );
470
471         my $appendedfieldscount = $marcrecord->append_fields( @marc_fields );
472         
473         diag $MARC::Record::ERROR if ( $MARC::Record::ERROR );
474         is( $appendedfieldscount, scalar @marc_fields, 'added correct number of MARC fields' );
475         
476         my $frameworkcode = ''; # XXX I'd like to put something reasonable here.
477         my ( $biblionumber, $biblioitemnumber ) = AddBiblio( $marcrecord, $frameworkcode );
478         ok( $biblionumber, "the biblionumber is $biblionumber" );
479         ok( $biblioitemnumber, "the biblioitemnumber is $biblioitemnumber" );
480         if ( $param{'add_items'} ) {
481             # my @iteminfo = AddItem( {}, $biblionumber );
482             my @iteminfo = AddItemFromMarc( $marcrecord, $biblionumber );
483             is( $iteminfo[0], $biblionumber,     "biblionumber is $biblionumber" );
484             is( $iteminfo[1], $biblioitemnumber, "biblioitemnumber is $biblioitemnumber" );
485             ok( $iteminfo[2], "itemnumber is $iteminfo[2]" );
486         push @{ $self->{'items'} },
487           { biblionumber     => $iteminfo[0],
488             biblioitemnumber => $iteminfo[1],
489             itemnumber       => $iteminfo[2],
490           };
491         }
492         push @{$self->{'biblios'}}, $biblionumber;
493     }
494    
495     $self->reindex_marc(); 
496     my $query = 'Finn Test';
497     my ( $error, $results ) = SimpleSearch( $query );
498     if ( $param{'count'} <= scalar( @$results ) ) {
499         pass( "found all $param{'count'} titles" );
500     } else {
501         fail( "we never found all $param{'count'} titles" );
502     }
503     
504 }
505
506 =head3 reindex_marc
507
508 Do a fast reindexing of all of the bib and authority
509 records and mark all zebraqueue entries done.
510
511 Useful for test routines that need to do a
512 lot of indexing without having to wait for
513 zebraqueue.
514
515 In NoZebra model, this only marks zebraqueue
516 done - the records should already be indexed.
517
518 =cut
519
520 sub reindex_marc {
521     my $self = shift;
522
523     # mark zebraqueue done regardless of the indexing mode
524     my $dbh = C4::Context->dbh();
525     $dbh->do("UPDATE zebraqueue SET done = 1 WHERE done = 0");
526
527     return if C4::Context->preference('NoZebra');
528
529     my $directory = tempdir(CLEANUP => 1);
530     foreach my $record_type qw(biblio authority) {
531         mkdir "$directory/$record_type";
532         my $sth = $dbh->prepare($record_type eq "biblio" ? "SELECT marc FROM biblioitems" : "SELECT marc FROM auth_header");
533         $sth->execute();
534         open OUT, ">:utf8", "$directory/$record_type/records";
535         while (my ($blob) = $sth->fetchrow_array) {
536             print OUT $blob;
537         }
538         close OUT;
539         my $zebra_server = "${record_type}server";
540         my $zebra_config  = C4::Context->zebraconfig($zebra_server)->{'config'};
541         my $zebra_db_dir  = C4::Context->zebraconfig($zebra_server)->{'directory'};
542         my $zebra_db = $record_type eq 'biblio' ? 'biblios' : 'authorities';
543         system "zebraidx -c $zebra_config -d $zebra_db -g iso2709 init > /dev/null 2>\&1";
544         system "zebraidx -c $zebra_config -d $zebra_db -g iso2709 update $directory/${record_type} > /dev/null 2>\&1";
545         system "zebraidx -c $zebra_config -d $zebra_db -g iso2709 commit > /dev/null 2>\&1";
546     }
547         
548 }
549
550
551 =head3 clear_test_database
552
553   removes all tables from test database so that install starts with a clean slate
554
555 =cut
556
557 sub clear_test_database {
558
559     diag "removing tables from test database";
560
561     my $dbh = C4::Context->dbh;
562     my $schema = C4::Context->config("database");
563
564     my @tables = get_all_tables($dbh, $schema);
565     foreach my $table (@tables) {
566         drop_all_foreign_keys($dbh, $table);
567     }
568
569     foreach my $table (@tables) {
570         drop_table($dbh, $table);
571     }
572 }
573
574 sub get_all_tables {
575   my ($dbh, $schema) = @_;
576   my $sth = $dbh->prepare("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?");
577   my @tables = ();
578   $sth->execute($schema);
579   while (my ($table) = $sth->fetchrow_array) {
580     push @tables, $table;
581   }
582   $sth->finish;
583   return @tables;
584 }
585
586 sub drop_all_foreign_keys {
587     my ($dbh, $table) = @_;
588     # get the table description
589     my $sth = $dbh->prepare("SHOW CREATE TABLE $table");
590     $sth->execute;
591     my $vsc_structure = $sth->fetchrow;
592     # split on CONSTRAINT keyword
593     my @fks = split /CONSTRAINT /,$vsc_structure;
594     # parse each entry
595     foreach (@fks) {
596         # isolate what is before FOREIGN KEY, if there is something, it's a foreign key to drop
597         $_ = /(.*) FOREIGN KEY.*/;
598         my $id = $1;
599         if ($id) {
600             # we have found 1 foreign, drop it
601             $dbh->do("ALTER TABLE $table DROP FOREIGN KEY $id");
602             if ( $dbh->err ) {
603                 diag "unable to DROP FOREIGN KEY '$id' on TABLE '$table' due to: " . $dbh->errstr();
604             }
605             undef $id;
606         }
607     }
608 }
609
610 sub drop_table {
611     my ($dbh, $table) = @_;
612     $dbh->do("DROP TABLE $table");
613     if ( $dbh->err ) {
614         diag "unable to drop table: '$table' due to: " . $dbh->errstr();
615     }
616 }
617
618 =head3 create_test_database
619
620   sets up the test database.
621
622 =cut
623
624 sub create_test_database {
625
626     diag 'creating testing database...';
627     my $installer = C4::Installer->new() or die 'unable to create new installer';
628     # warn Data::Dumper->Dump( [ $installer ], [ 'installer' ] );
629     my $all_languages = getAllLanguages();
630     my $error = $installer->load_db_schema();
631     die "unable to load_db_schema: $error" if ( $error );
632     my $list = $installer->sql_file_list('en', 'marc21', { optional  => 1,
633                                                            mandatory => 1 } );
634     my ($fwk_language, $installed_list) = $installer->load_sql_in_order($all_languages, @$list);
635     $installer->set_version_syspref();
636     $installer->set_marcflavour_syspref('MARC21');
637     $installer->set_indexing_engine(0);
638     diag 'database created.'
639 }
640
641
642 =head3 start_zebrasrv
643
644   This method deletes and reinitializes the zebra database directory,
645   and then spans off a zebra server.
646
647 =cut
648
649 sub start_zebrasrv {
650
651     stop_zebrasrv();
652     diag 'cleaning zebrasrv...';
653
654     foreach my $zebra_server ( qw( biblioserver authorityserver ) ) {
655         my $zebra_config  = C4::Context->zebraconfig($zebra_server)->{'config'};
656         my $zebra_db_dir  = C4::Context->zebraconfig($zebra_server)->{'directory'};
657         foreach my $zebra_db_name ( qw( biblios authorities ) ) {
658             my $command = "zebraidx -c $zebra_config -d $zebra_db_name init";
659             my $return = system( $command . ' > /dev/null 2>&1' );
660             if ( $return != 0 ) {
661                 diag( "command '$command' died with value: " . $? >> 8 );
662             }
663             
664             $command = "zebraidx -c $zebra_config -d $zebra_db_name create $zebra_db_name";
665             diag $command;
666             $return = system( $command . ' > /dev/null 2>&1' );
667             if ( $return != 0 ) {
668                 diag( "command '$command' died with value: " . $? >> 8 );
669             }
670         }
671     }
672     
673     diag 'starting zebrasrv...';
674
675     my $pidfile = File::Spec->catdir( C4::Context->config("logdir"), 'zebra.pid' );
676     my $command = sprintf( 'zebrasrv -f %s -D -l %s -p %s',
677                            $ENV{'KOHA_CONF'},
678                            File::Spec->catdir( C4::Context->config("logdir"), 'zebra.log' ),
679                            $pidfile,
680                       );
681     diag $command;
682     my $output = qx( $command );
683     if ( $output ) {
684         diag $output;
685     }
686     if ( -e $pidfile, 'pidfile exists' ) {
687         diag 'zebrasrv started.';
688     } else {
689         die 'unable to start zebrasrv';
690     }
691     return $output;
692 }
693
694 =head3 stop_zebrasrv
695
696   using the PID file for the zebra server, send it a TERM signal with
697   "kill". We can't tell if the process actually dies or not.
698
699 =cut
700
701 sub stop_zebrasrv {
702
703     my $pidfile = File::Spec->catdir( C4::Context->config("logdir"), 'zebra.pid' );
704     if ( -e $pidfile ) {
705         open( my $pidh, '<', $pidfile )
706           or return;
707         if ( defined $pidh ) {
708             my ( $pid ) = <$pidh> or return;
709             close $pidh;
710             my $killed = kill 15, $pid; # 15 is TERM
711             if ( $killed != 1 ) {
712                 warn "unable to kill zebrasrv with pid: $pid";
713             }
714         }
715     }
716 }
717
718
719 =head3 start_zebraqueue_daemon
720
721   kick off a zebraqueue_daemon.pl process.
722
723 =cut
724
725 sub start_zebraqueue_daemon {
726
727     my $command = q(run/bin/koha-zebraqueue-ctl.sh start);
728     diag $command;
729     my $started = system( $command );
730     diag "started: $started";
731     
732 }
733
734 =head3 stop_zebraqueue_daemon
735
736
737 =cut
738
739 sub stop_zebraqueue_daemon {
740
741     my $command = q(run/bin/koha-zebraqueue-ctl.sh stop);
742     diag $command;
743     my $started = system( $command );
744     diag "started: $started";
745
746 }
747
748 1;