Bug 18461: (bug 13757 follow-up) Do not mix decode_json and to_json
[koha.git] / t / lib / TestBuilder.pm
blob6509aee9da39c64da832abd55d73dc4fe819bd99
1 package t::lib::TestBuilder;
3 use Modern::Perl;
5 use Koha::Database;
7 use Carp;
8 use Module::Load;
9 use String::Random;
11 sub new {
12 my ($class) = @_;
13 my $self = {};
14 bless( $self, $class );
16 $self->schema( Koha::Database->new()->schema );
17 $self->schema->storage->sql_maker->quote_char('`');
19 $self->{gen_type} = _gen_type();
20 $self->{default_values} = _gen_default_values();
21 return $self;
24 sub schema {
25 my ($self, $schema) = @_;
27 if( defined( $schema ) ) {
28 $self->{schema} = $schema;
30 return $self->{schema};
33 # sub clear has been obsoleted; use delete_all from the schema resultset
35 sub delete {
36 my ( $self, $params ) = @_;
37 my $source = $params->{source} || return;
38 my @recs = ref( $params->{records} ) eq 'ARRAY'?
39 @{$params->{records}}: ( $params->{records} // () );
40 # tables without PK are not supported
41 my @pk = $self->schema->source( $source )->primary_columns;
42 return if !@pk;
43 my $rv = 0;
44 foreach my $rec ( @recs ) {
45 # delete only works when you supply full primary key values
46 # $cond does not include searches for undef (not allowed in PK)
47 my $cond = { map { defined $rec->{$_}? ($_, $rec->{$_}): (); } @pk };
48 next if keys %$cond < @pk;
49 $self->schema->resultset( $source )->search( $cond )->delete;
50 # we clear the pk columns in the supplied hash
51 # this indirectly signals at least an attempt to delete
52 map { delete $rec->{$_}; } @pk;
53 $rv++;
55 return $rv;
58 sub build_object {
59 my ( $self, $params ) = @_;
61 my $class = $params->{class};
62 my $value = $params->{value};
64 if ( not defined $class ) {
65 carp "Missing class param";
66 return;
69 load $class;
70 my $source = $class->_type;
71 my @pks = $self->schema->source( $class->_type )->primary_columns;
73 my $hashref = $self->build({ source => $source, value => $value });
74 my @ids;
76 foreach my $pk ( @pks ) {
77 push @ids, { $pk => $hashref->{ $pk } };
80 my $object = $class->find( @ids );
82 return $object;
85 sub build {
86 # build returns a hash of column values for a created record, or undef
87 # build does NOT update a record, or pass back values of an existing record
88 my ($self, $params) = @_;
89 my $source = $params->{source} || return;
90 my $value = $params->{value};
92 my $col_values = $self->_buildColumnValues({
93 source => $source,
94 value => $value,
95 });
96 return if !$col_values; # did not meet unique constraints?
98 # loop thru all fk and create linked records if needed
99 # fills remaining entries in $col_values
100 my $foreign_keys = $self->_getForeignKeys( { source => $source } );
101 for my $fk ( @$foreign_keys ) {
102 # skip when FK points to itself: e.g. borrowers:guarantorid
103 next if $fk->{source} eq $source;
104 my $keys = $fk->{keys};
105 my $tbl = $fk->{source};
106 my $res = $self->_create_links( $tbl, $keys, $col_values, $value );
107 return if !$res; # failed: no need to go further
108 foreach( keys %$res ) { # save new values
109 $col_values->{$_} = $res->{$_};
113 # store this record and return hashref
114 return $self->_storeColumnValues({
115 source => $source,
116 values => $col_values,
120 # ------------------------------------------------------------------------------
121 # Internal helper routines
123 sub _create_links {
124 # returns undef for failure to create linked records
125 # otherwise returns hashref containing new column values for parent record
126 my ( $self, $linked_tbl, $keys, $col_values, $value ) = @_;
128 my $fk_value = {};
129 my ( $cnt_scalar, $cnt_null ) = ( 0, 0 );
131 # First, collect all values for creating a linked record (if needed)
132 foreach my $fk ( @$keys ) {
133 my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
134 if( ref( $value->{$col} ) eq 'HASH' ) {
135 # add all keys from the FK hash
136 $fk_value = { %{ $value->{$col} }, %$fk_value };
138 if( exists $col_values->{$col} ) {
139 # add specific value (this does not necessarily exclude some
140 # values from the hash in the preceding if)
141 $fk_value->{ $destcol } = $col_values->{ $col };
142 $cnt_scalar++;
143 $cnt_null++ if !defined( $col_values->{$col} );
147 # If we saw all FK columns, first run the following checks
148 if( $cnt_scalar == @$keys ) {
149 # if one or more fk cols are null, the FK constraint will not be forced
150 return {} if $cnt_null > 0;
151 # does the record exist already?
152 return {} if $self->schema->resultset($linked_tbl)->find( $fk_value );
154 # create record with a recursive build call
155 my $row = $self->build({ source => $linked_tbl, value => $fk_value });
156 return if !$row; # failure
158 # Finally, only return the new values
159 my $rv = {};
160 foreach my $fk ( @$keys ) {
161 my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
162 next if exists $col_values->{ $col };
163 $rv->{ $col } = $row->{ $destcol };
165 return $rv; # success
168 sub _formatSource {
169 my ($params) = @_;
170 my $source = $params->{source} || return;
171 $source =~ s|(\w+)$|$1|;
172 return $source;
175 sub _buildColumnValues {
176 my ($self, $params) = @_;
177 my $source = _formatSource( $params ) || return;
178 my $original_value = $params->{value};
180 my $col_values = {};
181 my @columns = $self->schema->source($source)->columns;
182 my %unique_constraints = $self->schema->source($source)->unique_constraints();
184 my $build_value = 3;
185 # we try max three times if there are unique constraints
186 BUILD_VALUE: while ( $build_value ) {
187 # generate random values for all columns
188 for my $col_name( @columns ) {
189 my $valref = $self->_buildColumnValue({
190 source => $source,
191 column_name => $col_name,
192 value => $original_value,
194 return if !$valref; # failure
195 if( @$valref ) { # could be empty
196 # there will be only one value, but it could be undef
197 $col_values->{$col_name} = $valref->[0];
201 # verify the data would respect each unique constraint
202 # note that this is INCOMPLETE since not all col_values are filled
203 CONSTRAINTS: foreach my $constraint (keys %unique_constraints) {
205 my $condition;
206 my $constraint_columns = $unique_constraints{$constraint};
207 # loop through all constraint columns and build the condition
208 foreach my $constraint_column ( @$constraint_columns ) {
209 # build the filter
210 # if one column does not exist or is undef, skip it
211 # an insert with a null will not trigger the constraint
212 next CONSTRAINTS
213 if !exists $col_values->{ $constraint_column } ||
214 !defined $col_values->{ $constraint_column };
215 $condition->{ $constraint_column } =
216 $col_values->{ $constraint_column };
218 my $count = $self->schema
219 ->resultset( $source )
220 ->search( $condition )
221 ->count();
222 if ( $count > 0 ) {
223 # no point checking more stuff, exit the loop
224 $build_value--;
225 next BUILD_VALUE;
228 last; # you passed all tests
230 return $col_values if $build_value > 0;
232 # if you get here, we have a problem
233 warn "Violation of unique constraint in $source";
234 return;
237 sub _getForeignKeys {
239 # Returns the following arrayref
240 # [ [ source => name, keys => [ col_name => A, col_fk_name => B ] ], ... ]
241 # The array gives source name and keys for each FK constraint
243 my ($self, $params) = @_;
244 my $source = $self->schema->source( $params->{source} );
246 my ( @foreign_keys, $check_dupl );
247 my @relationships = $source->relationships;
248 for my $rel_name( @relationships ) {
249 my $rel_info = $source->relationship_info($rel_name);
250 if( $rel_info->{attrs}->{is_foreign_key_constraint} ) {
251 $rel_info->{source} =~ s/^.*:://g;
252 my $rel = { source => $rel_info->{source} };
254 my @keys;
255 while( my ($col_fk_name, $col_name) = each(%{$rel_info->{cond}}) ) {
256 $col_name =~ s|self.(\w+)|$1|;
257 $col_fk_name =~ s|foreign.(\w+)|$1|;
258 push @keys, {
259 col_name => $col_name,
260 col_fk_name => $col_fk_name,
263 # check if the combination table and keys is unique
264 # so skip double belongs_to relations (as in Biblioitem)
265 my $tag = $rel->{source}. ':'.
266 join ',', sort map { $_->{col_name} } @keys;
267 next if $check_dupl->{$tag};
268 $check_dupl->{$tag} = 1;
269 $rel->{keys} = \@keys;
270 push @foreign_keys, $rel;
273 return \@foreign_keys;
276 sub _storeColumnValues {
277 my ($self, $params) = @_;
278 my $source = $params->{source};
279 my $col_values = $params->{values};
280 my $new_row = $self->schema->resultset( $source )->create( $col_values );
281 return $new_row? { $new_row->get_columns }: {};
284 sub _buildColumnValue {
285 # returns an arrayref if all goes well
286 # an empty arrayref typically means: auto_incr column or fk column
287 # undef means failure
288 my ($self, $params) = @_;
289 my $source = $params->{source};
290 my $value = $params->{value};
291 my $col_name = $params->{column_name};
293 my $col_info = $self->schema->source($source)->column_info($col_name);
295 my $retvalue = [];
296 if( $col_info->{is_auto_increment} ) {
297 if( exists $value->{$col_name} ) {
298 warn "Value not allowed for auto_incr $col_name in $source";
299 return;
301 # otherwise: no need to assign a value
302 } elsif( $col_info->{is_foreign_key} || _should_be_fk($source,$col_name) ) {
303 if( exists $value->{$col_name} ) {
304 if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
305 # This explicit undef is not allowed
306 warn "Null value for $col_name in $source not allowed";
307 return;
309 if( ref( $value->{$col_name} ) ne 'HASH' ) {
310 push @$retvalue, $value->{$col_name};
312 # sub build will handle a passed hash value later on
314 } elsif( ref( $value->{$col_name} ) eq 'HASH' ) {
315 # this is not allowed for a column that is not a FK
316 warn "Hash not allowed for $col_name in $source";
317 return;
318 } elsif( exists $value->{$col_name} ) {
319 if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
320 # This explicit undef is not allowed
321 warn "Null value for $col_name in $source not allowed";
322 return;
324 push @$retvalue, $value->{$col_name};
325 } elsif( exists $self->{default_values}{$source}{$col_name} ) {
326 push @$retvalue, $self->{default_values}{$source}{$col_name};
327 } else {
328 my $data_type = $col_info->{data_type};
329 $data_type =~ s| |_|;
330 if( my $hdlr = $self->{gen_type}->{$data_type} ) {
331 push @$retvalue, &$hdlr( $self, { info => $col_info } );
332 } else {
333 warn "Unknown type $data_type for $col_name in $source";
334 return;
337 return $retvalue;
340 sub _should_be_fk {
341 # This sub is only needed for inconsistencies in the schema
342 # A column is not marked as FK, but a belongs_to relation is defined
343 my ( $source, $column ) = @_;
344 my $inconsistencies = {
345 'Item.biblionumber' => 1, #FIXME: Please remove me when I become FK
347 return $inconsistencies->{ "$source.$column" };
350 sub _gen_type {
351 return {
352 tinyint => \&_gen_int,
353 smallint => \&_gen_int,
354 mediumint => \&_gen_int,
355 integer => \&_gen_int,
356 bigint => \&_gen_int,
358 float => \&_gen_real,
359 decimal => \&_gen_real,
360 double_precision => \&_gen_real,
362 timestamp => \&_gen_datetime,
363 datetime => \&_gen_datetime,
364 date => \&_gen_date,
366 char => \&_gen_text,
367 varchar => \&_gen_text,
368 tinytext => \&_gen_text,
369 text => \&_gen_text,
370 mediumtext => \&_gen_text,
371 longtext => \&_gen_text,
373 set => \&_gen_set_enum,
374 enum => \&_gen_set_enum,
376 tinyblob => \&_gen_blob,
377 mediumblob => \&_gen_blob,
378 blob => \&_gen_blob,
379 longblob => \&_gen_blob,
383 sub _gen_int {
384 my ($self, $params) = @_;
385 my $data_type = $params->{info}->{data_type};
387 my $max = 1;
388 if( $data_type eq 'tinyint' ) {
389 $max = 127;
391 elsif( $data_type eq 'smallint' ) {
392 $max = 32767;
394 elsif( $data_type eq 'mediumint' ) {
395 $max = 8388607;
397 elsif( $data_type eq 'integer' ) {
398 $max = 2147483647;
400 elsif( $data_type eq 'bigint' ) {
401 $max = 9223372036854775807;
403 return int( rand($max+1) );
406 sub _gen_real {
407 my ($self, $params) = @_;
408 my $max = 10 ** 38;
409 if( defined( $params->{info}->{size} ) ) {
410 $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]);
412 return rand($max) + 1;
415 sub _gen_date {
416 my ($self, $params) = @_;
417 return $self->schema->storage->datetime_parser->format_date(DateTime->now())
420 sub _gen_datetime {
421 my ($self, $params) = @_;
422 return $self->schema->storage->datetime_parser->format_datetime(DateTime->now());
425 sub _gen_text {
426 my ($self, $params) = @_;
427 # From perldoc String::Random
428 # max: specify the maximum number of characters to return for * and other
429 # regular expression patters that don't return a fixed number of characters
430 my $regex = '[A-Za-z][A-Za-z0-9_]*';
431 my $size = $params->{info}{size};
432 if ( defined $size and $size > 1 ) {
433 $size--;
434 } elsif ( defined $size and $size == 1 ) {
435 $regex = '[A-Za-z]';
437 my $random = String::Random->new( max => $size );
438 return $random->randregex($regex);
441 sub _gen_set_enum {
442 my ($self, $params) = @_;
443 return $params->{info}->{extra}->{list}->[0];
446 sub _gen_blob {
447 my ($self, $params) = @_;;
448 return 'b';
451 sub _gen_default_values {
452 my ($self) = @_;
453 return {
454 Item => {
455 more_subfields_xml => undef,
460 =head1 NAME
462 t::lib::TestBuilder.pm - Koha module to create test records
464 =head1 SYNOPSIS
466 use t::lib::TestBuilder;
467 my $builder = t::lib::TestBuilder->new;
469 # The following call creates a patron, linked to branch CPL.
470 # Surname is provided, other columns are randomly generated.
471 # Branch CPL is created if it does not exist.
472 my $patron = $builder->build({
473 source => 'Borrower',
474 value => { surname => 'Jansen', branchcode => 'CPL' },
477 =head1 DESCRIPTION
479 This module automatically creates database records for you.
480 If needed, records for foreign keys are created too.
481 Values will be randomly generated if not passed to TestBuilder.
482 Note that you should wrap these actions in a transaction yourself.
484 =head1 METHODS
486 =head2 new
488 my $builder = t::lib::TestBuilder->new;
490 Constructor - Returns the object TestBuilder
492 =head2 schema
494 my $schema = $builder->schema;
496 Getter - Returns the schema of DBIx::Class
498 =head2 delete
500 $builder->delete({
501 source => $source,
502 records => $patron, # OR: records => [ $patron, ... ],
505 Delete individual records, created by builder.
506 Returns the number of delete attempts, or undef.
508 =head2 build
510 $builder->build({ source => $source_name, value => $value });
512 Create a test record in the table, represented by $source_name.
513 The name is required and must conform to the DBIx::Class schema.
514 Values may be specified by the optional $value hashref. Will be
515 randomized otherwise.
516 If needed, TestBuilder creates linked records for foreign keys.
517 Returns the values of the new record as a hashref, or undef if
518 the record could not be created.
520 Note that build also supports recursive hash references inside the
521 value hash for foreign key columns, like:
522 value => {
523 column1 => 'some_value',
524 fk_col2 => {
525 columnA => 'another_value',
528 The hash for fk_col2 here means: create a linked record with build
529 where columnA has this value. In case of a composite FK the hashes
530 are merged.
532 Realize that passing primary key values to build may result in undef
533 if a record with that primary key already exists.
535 =head2 build_object
537 Given a plural Koha::Object-derived class, it creates a random element, and
538 returns the corresponding Koha::Object.
540 my $patron = $builder->build_object({ class => 'Koha::Patrons' [, value => { ... }] });
542 =head1 AUTHOR
544 Yohann Dufour <yohann.dufour@biblibre.com>
546 Koha Development Team
548 =head1 COPYRIGHT
550 Copyright 2014 - Biblibre SARL
552 =head1 LICENSE
554 This file is part of Koha.
556 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
557 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
559 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.
561 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.
563 =cut