Bug 26033: Use pic.infini.fr instead of framapic
[koha.git] / t / lib / TestBuilder.pm
1 package t::lib::TestBuilder;
2
3 use Modern::Perl;
4
5 use Koha::Database;
6 use C4::Biblio;
7 use C4::Items;
8 use Koha::Biblios;
9 use Koha::Items;
10 use Koha::DateUtils qw( dt_from_string );
11
12 use Bytes::Random::Secure;
13 use Carp;
14 use Module::Load;
15 use String::Random;
16
17 use constant {
18     SIZE_BARCODE => 20, # Not perfect but avoid to fetch the value when creating a new item
19 };
20
21 sub new {
22     my ($class) = @_;
23     my $self = {};
24     bless( $self, $class );
25
26     $self->schema( Koha::Database->new()->schema );
27     $self->schema->storage->sql_maker->quote_char('`');
28
29     $self->{gen_type} = _gen_type();
30     $self->{default_values} = _gen_default_values();
31     return $self;
32 }
33
34 sub schema {
35     my ($self, $schema) = @_;
36
37     if( defined( $schema ) ) {
38         $self->{schema} = $schema;
39     }
40     return $self->{schema};
41 }
42
43 # sub clear has been obsoleted; use delete_all from the schema resultset
44
45 sub delete {
46     my ( $self, $params ) = @_;
47     my $source = $params->{source} || return;
48     my @recs = ref( $params->{records} ) eq 'ARRAY'?
49         @{$params->{records}}: ( $params->{records} // () );
50     # tables without PK are not supported
51     my @pk = $self->schema->source( $source )->primary_columns;
52     return if !@pk;
53     my $rv = 0;
54     foreach my $rec ( @recs ) {
55     # delete only works when you supply full primary key values
56     # $cond does not include searches for undef (not allowed in PK)
57         my $cond = { map { defined $rec->{$_}? ($_, $rec->{$_}): (); } @pk };
58         next if keys %$cond < @pk;
59         $self->schema->resultset( $source )->search( $cond )->delete;
60         # we clear the pk columns in the supplied hash
61         # this indirectly signals at least an attempt to delete
62         map { delete $rec->{$_}; } @pk;
63         $rv++;
64     }
65     return $rv;
66 }
67
68 sub build_object {
69     my ( $self, $params ) = @_;
70
71     my $class = $params->{class};
72     my $value = $params->{value};
73
74     if ( not defined $class ) {
75         carp "Missing class param";
76         return;
77     }
78
79     my @unknowns = grep( !/^(class|value)$/, keys %{ $params });
80     carp "Unknown parameter(s): ", join( ', ', @unknowns ) if scalar @unknowns;
81
82     load $class;
83     my $source = $class->_type;
84
85     my $hashref = $self->build({ source => $source, value => $value });
86     my $object;
87     if ( $class eq 'Koha::Old::Patrons' ) {
88         $object = $class->search({ borrowernumber => $hashref->{borrowernumber} })->next;
89     } elsif ( $class eq 'Koha::Statistics' ) {
90         $object = $class->search({ datetime => $hashref->{datetime} })->next;
91     } else {
92         my @ids;
93         my @pks = $self->schema->source( $class->_type )->primary_columns;
94         foreach my $pk ( @pks ) {
95             push @ids, $hashref->{ $pk };
96         }
97
98         $object = $class->find( @ids );
99     }
100
101     return $object;
102 }
103
104 sub build {
105 # build returns a hash of column values for a created record, or undef
106 # build does NOT update a record, or pass back values of an existing record
107     my ($self, $params) = @_;
108     my $source  = $params->{source};
109     if( !$source ) {
110         carp "Source parameter not specified!";
111         return;
112     }
113     my $value   = $params->{value};
114
115     my @unknowns = grep( !/^(source|value)$/, keys %{ $params });
116     carp "Unknown parameter(s): ", join( ', ', @unknowns ) if scalar @unknowns;
117
118     my $col_values = $self->_buildColumnValues({
119         source  => $source,
120         value   => $value,
121     });
122     return if !$col_values; # did not meet unique constraints?
123
124     # loop thru all fk and create linked records if needed
125     # fills remaining entries in $col_values
126     my $foreign_keys = $self->_getForeignKeys( { source => $source } );
127     for my $fk ( @$foreign_keys ) {
128         # skip when FK points to itself: e.g. borrowers:guarantorid
129         next if $fk->{source} eq $source;
130         my $keys = $fk->{keys};
131         my $tbl = $fk->{source};
132         my $res = $self->_create_links( $tbl, $keys, $col_values, $value );
133         return if !$res; # failed: no need to go further
134         foreach( keys %$res ) { # save new values
135             $col_values->{$_} = $res->{$_};
136         }
137     }
138
139     # store this record and return hashref
140     return $self->_storeColumnValues({
141         source => $source,
142         values => $col_values,
143     });
144 }
145
146 sub build_sample_biblio {
147     my ( $self, $args ) = @_;
148
149     my $title  = $args->{title}  || 'Some boring read';
150     my $author = $args->{author} || 'Some boring author';
151     my $frameworkcode = $args->{frameworkcode} || '';
152     my $itemtype = $args->{itemtype}
153       || $self->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
154
155     my $marcflavour = C4::Context->preference('marcflavour');
156
157     my $record = MARC::Record->new();
158     my ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'a' ) : ( 245, 'a' );
159     $record->append_fields(
160         MARC::Field->new( $tag, ' ', ' ', $subfield => $title ),
161     );
162
163     ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'f' ) : ( 100, 'a' );
164     $record->append_fields(
165         MARC::Field->new( $tag, ' ', ' ', $subfield => $author ),
166     );
167
168     ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 995, 'r' ) : ( 942, 'c' );
169     $record->append_fields(
170         MARC::Field->new( $tag, ' ', ' ', $subfield => $itemtype )
171     );
172
173     my ($biblio_id) = C4::Biblio::AddBiblio( $record, $frameworkcode );
174     return Koha::Biblios->find($biblio_id);
175 }
176
177 sub build_sample_item {
178     my ( $self, $args ) = @_;
179
180     my $biblionumber =
181       delete $args->{biblionumber} || $self->build_sample_biblio->biblionumber;
182     my $library = delete $args->{library}
183       || $self->build_object( { class => 'Koha::Libraries' } )->branchcode;
184
185     # If itype is not passed it will be picked from the biblio (see Koha::Item->store)
186
187     my $barcode = delete $args->{barcode}
188       || $self->_gen_text( { info => { size => SIZE_BARCODE } } );
189
190     return Koha::Item->new(
191         {
192             biblionumber  => $biblionumber,
193             homebranch    => $library,
194             holdingbranch => $library,
195             barcode       => $barcode,
196             %$args,
197         }
198     )->store->get_from_storage;
199 }
200
201 # ------------------------------------------------------------------------------
202 # Internal helper routines
203
204 sub _create_links {
205 # returns undef for failure to create linked records
206 # otherwise returns hashref containing new column values for parent record
207     my ( $self, $linked_tbl, $keys, $col_values, $value ) = @_;
208
209     my $fk_value = {};
210     my ( $cnt_scalar, $cnt_null ) = ( 0, 0 );
211
212     # First, collect all values for creating a linked record (if needed)
213     foreach my $fk ( @$keys ) {
214         my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
215         if( ref( $value->{$col} ) eq 'HASH' ) {
216             # add all keys from the FK hash
217             $fk_value = { %{ $value->{$col} }, %$fk_value };
218         }
219         if( exists $col_values->{$col} ) {
220             # add specific value (this does not necessarily exclude some
221             # values from the hash in the preceding if)
222             $fk_value->{ $destcol } = $col_values->{ $col };
223             $cnt_scalar++;
224             $cnt_null++ if !defined( $col_values->{$col} );
225         }
226     }
227
228     # If we saw all FK columns, first run the following checks
229     if( $cnt_scalar == @$keys ) {
230         # if one or more fk cols are null, the FK constraint will not be forced
231         return {} if $cnt_null > 0;
232         # does the record exist already?
233         return {} if $self->schema->resultset($linked_tbl)->find( $fk_value );
234     }
235     # create record with a recursive build call
236     my $row = $self->build({ source => $linked_tbl, value => $fk_value });
237     return if !$row; # failure
238
239     # Finally, only return the new values
240     my $rv = {};
241     foreach my $fk ( @$keys ) {
242         my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
243         next if exists $col_values->{ $col };
244         $rv->{ $col } = $row->{ $destcol };
245     }
246     return $rv; # success
247 }
248
249 sub _formatSource {
250     my ($params) = @_;
251     my $source = $params->{source} || return;
252     $source =~ s|(\w+)$|$1|;
253     return $source;
254 }
255
256 sub _buildColumnValues {
257     my ($self, $params) = @_;
258     my $source = _formatSource( $params ) || return;
259     my $original_value = $params->{value};
260
261     my $col_values = {};
262     my @columns = $self->schema->source($source)->columns;
263     my %unique_constraints = $self->schema->source($source)->unique_constraints();
264
265     my $build_value = 5;
266     # we try max $build_value times if there are unique constraints
267     BUILD_VALUE: while ( $build_value ) {
268         # generate random values for all columns
269         for my $col_name( @columns ) {
270             my $valref = $self->_buildColumnValue({
271                 source      => $source,
272                 column_name => $col_name,
273                 value       => $original_value,
274             });
275             return if !$valref; # failure
276             if( @$valref ) { # could be empty
277                 # there will be only one value, but it could be undef
278                 $col_values->{$col_name} = $valref->[0];
279             }
280         }
281
282         # verify the data would respect each unique constraint
283         # note that this is INCOMPLETE since not all col_values are filled
284         CONSTRAINTS: foreach my $constraint (keys %unique_constraints) {
285
286                 my $condition;
287                 my $constraint_columns = $unique_constraints{$constraint};
288                 # loop through all constraint columns and build the condition
289                 foreach my $constraint_column ( @$constraint_columns ) {
290                     # build the filter
291                     # if one column does not exist or is undef, skip it
292                     # an insert with a null will not trigger the constraint
293                     next CONSTRAINTS
294                         if !exists $col_values->{ $constraint_column } ||
295                         !defined $col_values->{ $constraint_column };
296                     $condition->{ $constraint_column } =
297                             $col_values->{ $constraint_column };
298                 }
299                 my $count = $self->schema
300                                  ->resultset( $source )
301                                  ->search( $condition )
302                                  ->count();
303                 if ( $count > 0 ) {
304                     # no point checking more stuff, exit the loop
305                     $build_value--;
306                     next BUILD_VALUE;
307                 }
308         }
309         last; # you passed all tests
310     }
311     return $col_values if $build_value > 0;
312
313     # if you get here, we have a problem
314     warn "Violation of unique constraint in $source";
315     return;
316 }
317
318 sub _getForeignKeys {
319
320 # Returns the following arrayref
321 #   [ [ source => name, keys => [ col_name => A, col_fk_name => B ] ], ... ]
322 # The array gives source name and keys for each FK constraint
323
324     my ($self, $params) = @_;
325     my $source = $self->schema->source( $params->{source} );
326
327     my ( @foreign_keys, $check_dupl );
328     my @relationships = $source->relationships;
329     for my $rel_name( @relationships ) {
330         my $rel_info = $source->relationship_info($rel_name);
331         if( $rel_info->{attrs}->{is_foreign_key_constraint} ) {
332             $rel_info->{source} =~ s/^.*:://g;
333             my $rel = { source => $rel_info->{source} };
334
335             my @keys;
336             while( my ($col_fk_name, $col_name) = each(%{$rel_info->{cond}}) ) {
337                 $col_name    =~ s|self.(\w+)|$1|;
338                 $col_fk_name =~ s|foreign.(\w+)|$1|;
339                 push @keys, {
340                     col_name    => $col_name,
341                     col_fk_name => $col_fk_name,
342                 };
343             }
344             # check if the combination table and keys is unique
345             # so skip double belongs_to relations (as in Biblioitem)
346             my $tag = $rel->{source}. ':'.
347                 join ',', sort map { $_->{col_name} } @keys;
348             next if $check_dupl->{$tag};
349             $check_dupl->{$tag} = 1;
350             $rel->{keys} = \@keys;
351             push @foreign_keys, $rel;
352         }
353     }
354     return \@foreign_keys;
355 }
356
357 sub _storeColumnValues {
358     my ($self, $params) = @_;
359     my $source      = $params->{source};
360     my $col_values  = $params->{values};
361     my $new_row = $self->schema->resultset( $source )->create( $col_values );
362     return $new_row? { $new_row->get_columns }: {};
363 }
364
365 sub _buildColumnValue {
366 # returns an arrayref if all goes well
367 # an empty arrayref typically means: auto_incr column or fk column
368 # undef means failure
369     my ($self, $params) = @_;
370     my $source    = $params->{source};
371     my $value     = $params->{value};
372     my $col_name  = $params->{column_name};
373
374     my $col_info  = $self->schema->source($source)->column_info($col_name);
375
376     my $retvalue = [];
377     if( $col_info->{is_auto_increment} ) {
378         if( exists $value->{$col_name} ) {
379             warn "Value not allowed for auto_incr $col_name in $source";
380             return;
381         }
382         # otherwise: no need to assign a value
383     } elsif( $col_info->{is_foreign_key} || _should_be_fk($source,$col_name) ) {
384         if( exists $value->{$col_name} ) {
385             if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
386                 # This explicit undef is not allowed
387                 warn "Null value for $col_name in $source not allowed";
388                 return;
389             }
390             if( ref( $value->{$col_name} ) ne 'HASH' ) {
391                 push @$retvalue, $value->{$col_name};
392             }
393             # sub build will handle a passed hash value later on
394         }
395     } elsif( ref( $value->{$col_name} ) eq 'HASH' ) {
396         # this is not allowed for a column that is not a FK
397         warn "Hash not allowed for $col_name in $source";
398         return;
399     } elsif( exists $value->{$col_name} ) {
400         if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
401             # This explicit undef is not allowed
402             warn "Null value for $col_name in $source not allowed";
403             return;
404         }
405         push @$retvalue, $value->{$col_name};
406     } elsif( exists $self->{default_values}{$source}{$col_name} ) {
407         my $v = $self->{default_values}{$source}{$col_name};
408         $v = &$v() if ref($v) eq 'CODE';
409         push @$retvalue, $v;
410     } else {
411         my $data_type = $col_info->{data_type};
412         $data_type =~ s| |_|;
413         if( my $hdlr = $self->{gen_type}->{$data_type} ) {
414             push @$retvalue, &$hdlr( $self, { info => $col_info } );
415         } else {
416             warn "Unknown type $data_type for $col_name in $source";
417             return;
418         }
419     }
420     return $retvalue;
421 }
422
423 sub _should_be_fk {
424 # This sub is only needed for inconsistencies in the schema
425 # A column is not marked as FK, but a belongs_to relation is defined
426     my ( $source, $column ) = @_;
427     my $inconsistencies = {
428         'Item.biblionumber' => 1, #FIXME: Please remove me when I become FK
429     };
430     return $inconsistencies->{ "$source.$column" };
431 }
432
433 sub _gen_type {
434     return {
435         tinyint   => \&_gen_int,
436         smallint  => \&_gen_int,
437         mediumint => \&_gen_int,
438         integer   => \&_gen_int,
439         bigint    => \&_gen_int,
440
441         float            => \&_gen_real,
442         decimal          => \&_gen_real,
443         double_precision => \&_gen_real,
444
445         timestamp => \&_gen_datetime,
446         datetime  => \&_gen_datetime,
447         date      => \&_gen_date,
448
449         char       => \&_gen_text,
450         varchar    => \&_gen_text,
451         tinytext   => \&_gen_text,
452         text       => \&_gen_text,
453         mediumtext => \&_gen_text,
454         longtext   => \&_gen_text,
455
456         set  => \&_gen_set_enum,
457         enum => \&_gen_set_enum,
458
459         tinyblob   => \&_gen_blob,
460         mediumblob => \&_gen_blob,
461         blob       => \&_gen_blob,
462         longblob   => \&_gen_blob,
463     };
464 };
465
466 sub _gen_int {
467     my ($self, $params) = @_;
468     my $data_type = $params->{info}->{data_type};
469
470     my $max = 1;
471     if( $data_type eq 'tinyint' ) {
472         $max = 127;
473     }
474     elsif( $data_type eq 'smallint' ) {
475         $max = 32767;
476     }
477     elsif( $data_type eq 'mediumint' ) {
478         $max = 8388607;
479     }
480     elsif( $data_type eq 'integer' ) {
481         $max = 2147483647;
482     }
483     elsif( $data_type eq 'bigint' ) {
484         $max = 9223372036854775807;
485     }
486     return int( rand($max+1) );
487 }
488
489 sub _gen_real {
490     my ($self, $params) = @_;
491     my $max = 10 ** 38;
492     if( defined( $params->{info}->{size} ) ) {
493         $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]);
494     }
495     $max = 10 ** 5 if $max > 10 ** 5;
496     return sprintf("%.2f", rand($max-0.1));
497 }
498
499 sub _gen_date {
500     my ($self, $params) = @_;
501     return $self->schema->storage->datetime_parser->format_date(dt_from_string)
502 }
503
504 sub _gen_datetime {
505     my ($self, $params) = @_;
506     return $self->schema->storage->datetime_parser->format_datetime(dt_from_string);
507 }
508
509 sub _gen_text {
510     my ($self, $params) = @_;
511     # From perldoc String::Random
512     my $size = $params->{info}{size} // 10;
513     $size -= alt_rand(0.5 * $size);
514     my $regex = $size > 1
515         ? '[A-Za-z][A-Za-z0-9_]{'.($size-1).'}'
516         : '[A-Za-z]';
517     my $random = String::Random->new( rand_gen => \&alt_rand );
518     # rand_gen is only supported from 0.27 onward
519     return $random->randregex($regex);
520 }
521
522 sub alt_rand { #Alternative randomizer
523     my ($max) = @_;
524     my $random = Bytes::Random::Secure->new( NonBlocking => 1 );
525     my $r = $random->irand / 2**32;
526     return int( $r * $max );
527 }
528
529 sub _gen_set_enum {
530     my ($self, $params) = @_;
531     return $params->{info}->{extra}->{list}->[0];
532 }
533
534 sub _gen_blob {
535     my ($self, $params) = @_;;
536     return 'b';
537 }
538
539 sub _gen_default_values {
540     my ($self) = @_;
541     return {
542         Borrower => {
543             login_attempts => 0,
544             gonenoaddress  => undef,
545             lost           => undef,
546             debarred       => undef,
547             borrowernotes  => '',
548         },
549         Item => {
550             notforloan         => 0,
551             itemlost           => 0,
552             withdrawn          => 0,
553             restricted         => 0,
554             damaged            => 0,
555             more_subfields_xml => undef,
556         },
557         Category => {
558             enrolmentfee => 0,
559             reservefee   => 0,
560             # Not X, used for statistics
561             category_type => sub { return [ qw( A C S I P ) ]->[int(rand(5))] },
562         },
563         Branch => {
564             pickup_location => 0,
565         },
566         Itemtype => {
567             rentalcharge => 0,
568             rentalcharge_daily => 0,
569             rentalcharge_hourly => 0,
570             defaultreplacecost => 0,
571             processfee => 0,
572             notforloan => 0,
573         },
574         Aqbookseller => {
575             tax_rate => 0,
576             discount => 0,
577         },
578         AuthHeader => {
579             marcxml => '',
580         },
581         RefundLostItemFeeRules => {
582             rule_name => 'refund',
583         },
584     };
585 }
586
587 =head1 NAME
588
589 t::lib::TestBuilder.pm - Koha module to create test records
590
591 =head1 SYNOPSIS
592
593     use t::lib::TestBuilder;
594     my $builder = t::lib::TestBuilder->new;
595
596     # The following call creates a patron, linked to branch CPL.
597     # Surname is provided, other columns are randomly generated.
598     # Branch CPL is created if it does not exist.
599     my $patron = $builder->build({
600         source => 'Borrower',
601         value  => { surname => 'Jansen', branchcode => 'CPL' },
602     });
603
604 =head1 DESCRIPTION
605
606 This module automatically creates database records for you.
607 If needed, records for foreign keys are created too.
608 Values will be randomly generated if not passed to TestBuilder.
609 Note that you should wrap these actions in a transaction yourself.
610
611 =head1 METHODS
612
613 =head2 new
614
615     my $builder = t::lib::TestBuilder->new;
616
617     Constructor - Returns the object TestBuilder
618
619 =head2 schema
620
621     my $schema = $builder->schema;
622
623     Getter - Returns the schema of DBIx::Class
624
625 =head2 delete
626
627     $builder->delete({
628         source => $source,
629         records => $patron, # OR: records => [ $patron, ... ],
630     });
631
632     Delete individual records, created by builder.
633     Returns the number of delete attempts, or undef.
634
635 =head2 build
636
637     $builder->build({ source  => $source_name, value => $value });
638
639     Create a test record in the table, represented by $source_name.
640     The name is required and must conform to the DBIx::Class schema.
641     Values may be specified by the optional $value hashref. Will be
642     randomized otherwise.
643     If needed, TestBuilder creates linked records for foreign keys.
644     Returns the values of the new record as a hashref, or undef if
645     the record could not be created.
646
647     Note that build also supports recursive hash references inside the
648     value hash for foreign key columns, like:
649         value => {
650             column1 => 'some_value',
651             fk_col2 => {
652                 columnA => 'another_value',
653             }
654         }
655     The hash for fk_col2 here means: create a linked record with build
656     where columnA has this value. In case of a composite FK the hashes
657     are merged.
658
659     Realize that passing primary key values to build may result in undef
660     if a record with that primary key already exists.
661
662 =head2 build_object
663
664 Given a plural Koha::Object-derived class, it creates a random element, and
665 returns the corresponding Koha::Object.
666
667     my $patron = $builder->build_object({ class => 'Koha::Patrons' [, value => { ... }] });
668
669 =head1 AUTHOR
670
671 Yohann Dufour <yohann.dufour@biblibre.com>
672
673 Koha Development Team
674
675 =head1 COPYRIGHT
676
677 Copyright 2014 - Biblibre SARL
678
679 =head1 LICENSE
680
681 This file is part of Koha.
682
683 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
684 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
685
686 Koha is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
687
688 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.
689
690 =cut
691
692 1;