1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
56 use base 'Test::More';
59 # Warn about role consumers overriding role methods so we catch it in tests.
60 $ENV{PERL_ROLE_OVERRIDE_WARN} = 1;
63 # We use the Test::NoWarnings catching and reporting functionality, but need to
64 # wrap it in our own special handler because of the warn handler installed via
66 require Test::NoWarnings;
68 my $Test_NoWarnings_Catcher = $SIG{__WARN__};
69 my $check_warnings_in_end = 1;
72 use File::Temp qw(tempfile);
73 use File::Path qw(mkpath);
76 use Scalar::Util qw(blessed);
78 our @EXPORT = qw(is_empty diag parse_mail works fails plan done_testing);
99 To run the rt test suite with coverage support, install L<Devel::Cover> and run:
101 make test RT_DBA_USER=.. RT_DBA_PASSWORD=.. HARNESS_PERL_SWITCHES=-MDevel::Cover
102 cover -ignore_re '^var/mason_data/' -ignore_re '^t/'
104 The coverage tests have DevelMode turned off, and have
105 C<named_component_subs> enabled for L<HTML::Mason> to avoid an optimizer
106 problem in Perl that hides the top-level optree from L<Devel::Cover>.
114 delete $ENV{$_} for qw/LANGUAGE LC_ALL LC_MESSAGES LANG/;
120 my %args = %rttest_opt = @_;
122 $rttest_opt{'nodb'} = $args{'nodb'} = 1 if $^C;
124 # Spit out a plan (if we got one) *before* we load modules
125 if ( $args{'tests'} ) {
126 plan( tests => $args{'tests'} )
127 unless $args{'tests'} eq 'no_declare';
129 elsif ( exists $args{'tests'} ) {
130 # do nothing if they say "tests => undef" - let them make the plan
132 elsif ( $args{'skip_all'} ) {
133 plan(skip_all => $args{'skip_all'});
136 $class->builder->no_plan unless $class->builder->has_plan;
139 push @{ $args{'plugins'} ||= [] }, @{ $args{'requires'} }
140 if $args{'requires'};
141 push @{ $args{'plugins'} ||= [] }, $args{'testing'}
143 push @{ $args{'plugins'} ||= [] }, split " ", $ENV{RT_TEST_PLUGINS}
144 if $ENV{RT_TEST_PLUGINS};
146 $class->bootstrap_tempdir;
148 $class->bootstrap_port;
150 $class->bootstrap_plugins_paths( %args );
152 $class->bootstrap_config( %args );
158 RT::InitPluginPaths();
162 $class->bootstrap_db( %args );
171 RT->Config->PostLoadCheck;
173 $class->set_config_wrapper;
175 $class->encode_output;
177 my $screen_logger = $RT::Logger->remove( 'screen' );
178 require Log::Dispatch::Perl;
179 $RT::Logger->add( Log::Dispatch::Perl->new
181 min_level => $screen_logger->min_level,
182 action => { error => 'warn',
183 critical => 'warn' } ) );
185 # XXX: this should really be totally isolated environment so we
186 # can parallelize and be sane
187 mkpath [ $RT::MasonSessionDir ]
188 if RT->Config->Get('DatabaseType');
191 while ( my ($package) = caller($level-1) ) {
192 last unless $package =~ /Test/;
196 # By default we test HTML templates, but text templates are
197 # available on request
198 if ( $args{'text_templates'} ) {
199 $class->switch_templates_ok('text');
202 Test::More->export_to_level($level);
203 Test::NoWarnings->export_to_level($level);
205 # Blow away symbols we redefine to avoid warnings.
206 # better than "no warnings 'redefine'" because we might accidentally
207 # suppress a mistaken redefinition
209 delete ${ caller($level) . '::' }{diag};
210 delete ${ caller($level) . '::' }{plan};
211 delete ${ caller($level) . '::' }{done_testing};
212 __PACKAGE__->export_to_level($level);
217 local $Test::Builder::Level = $Test::Builder::Level + 1;
218 return Test::More::ok(1, $d) unless defined $v;
219 return Test::More::ok(1, $d) unless length $v;
220 return Test::More::is($v, '', $d);
223 my $created_new_db; # have we created new db? mainly for parallel testing
225 sub db_requires_no_dba {
227 my $db_type = RT->Config->Get('DatabaseType');
228 return 1 if $db_type eq 'SQLite';
236 # Determine which ports are in use
237 use Fcntl qw(:DEFAULT :flock);
238 my $portfile = "$tmp{'directory'}/../ports";
239 sysopen(PORTS, $portfile, O_RDWR|O_CREAT)
240 or die "Can't write to ports file $portfile: $!";
241 flock(PORTS, LOCK_EX)
242 or die "Can't write-lock ports file $portfile: $!";
243 $ports{$_}++ for split ' ', join("",<PORTS>);
245 # Pick a random port, checking that the port isn't in our in-use
246 # list, and that something isn't already listening there.
248 $port = 1024 + int rand(10_000) + $$ % 1024;
249 redo if $ports{$port};
251 # There is a race condition in here, where some non-RT::Test
252 # process claims the port after we check here but before our
253 # server binds. However, since we mostly care about race
254 # conditions with ourselves under high concurrency, this is
255 # generally good enough.
256 my $paddr = sockaddr_in( $port, inet_aton('localhost') );
257 socket( SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp') )
259 if ( connect( SOCK, $paddr ) ) {
268 # Write back out the in-use ports
271 print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
272 close(PORTS) or die "Can't close ports file: $!";
275 sub bootstrap_tempdir {
277 my ($test_dir, $test_file) = ('t', '');
279 if (File::Spec->rel2abs($0) =~ m{(?:^|[\\/])(x?t)[/\\](.*)}) {
282 $test_file =~ s{[/\\]}{-}g;
285 my $dir_name = File::Spec->rel2abs("$test_dir/tmp");
287 return $tmp{'directory'} = File::Temp->newdir(
288 "${test_file}XXXXXXXX",
293 sub bootstrap_config {
297 $tmp{'config'}{'RT'} = File::Spec->catfile(
298 "$tmp{'directory'}", 'RT_SiteConfig.pm'
300 open( my $config, '>', $tmp{'config'}{'RT'} )
301 or die "Couldn't open $tmp{'config'}{'RT'}: $!";
303 my $dbname = $ENV{RT_TEST_PARALLEL}? "rt4test_$port" : "rt4test";
305 Set( \$WebDomain, "localhost");
306 Set( \$WebPort, $port);
308 Set( \@LexiconLanguages, qw(en zh_TW zh_CN fr ja));
309 Set( \$RTAddressRegexp , qr/^bad_re_that_doesnt_match\$/i);
310 Set( \$ShowHistory, "always");
312 if ( $ENV{'RT_TEST_DB_SID'} ) { # oracle case
313 print $config "Set( \$DatabaseName , '$ENV{'RT_TEST_DB_SID'}' );\n";
314 print $config "Set( \$DatabaseUser , '$dbname');\n";
316 print $config "Set( \$DatabaseName , '$dbname');\n";
317 print $config "Set( \$DatabaseUser , 'u${dbname}');\n";
319 if ( $ENV{'RT_TEST_DB_HOST'} ) {
320 print $config "Set( \$DatabaseHost , '$ENV{'RT_TEST_DB_HOST'}');\n";
323 if ( $args{'plugins'} ) {
324 print $config "Set( \@Plugins, qw(". join( ' ', @{ $args{'plugins'} } ) .") );\n";
326 my $plugin_data = File::Spec->rel2abs("t/data/plugins");
327 print $config qq[\$RT::PluginPath = "$plugin_data";\n];
330 if ( $INC{'Devel/Cover.pm'} ) {
331 print $config "Set( \$DevelMode, 0 );\n";
333 elsif ( $ENV{RT_TEST_DEVEL} ) {
334 print $config "Set( \$DevelMode, 1 );\n";
337 print $config "Set( \$DevelMode, 0 );\n";
340 $self->bootstrap_logging( $config );
343 my $mail_catcher = $tmp{'mailbox'} = File::Spec->catfile(
344 $tmp{'directory'}->dirname, 'mailbox.eml'
347 Set( \$MailCommand, sub {
350 open( my \$handle, '>>', '$mail_catcher' )
351 or die "Unable to open '$mail_catcher' for appending: \$!";
353 \$MIME->print(\$handle);
354 print \$handle "%% split me! %%\n";
359 $self->bootstrap_more_config($config, \%args);
361 print $config $args{'config'} if $args{'config'};
363 print $config "\n1;\n";
364 $ENV{'RT_SITE_CONFIG'} = $tmp{'config'}{'RT'};
370 sub bootstrap_more_config { }
372 sub bootstrap_logging {
376 # prepare file for logging
377 $tmp{'log'}{'RT'} = File::Spec->catfile(
378 "$tmp{'directory'}", 'rt.debug.log'
380 open( my $fh, '>', $tmp{'log'}{'RT'} )
381 or die "Couldn't open $tmp{'config'}{'RT'}: $!";
382 # make world writable so apache under different user
384 chmod 0666, $tmp{'log'}{'RT'};
387 Set( \$LogToSyslog , undef);
388 Set( \$LogToSTDERR , "warning");
389 Set( \$LogToFile, 'debug' );
390 Set( \$LogDir, q{$tmp{'directory'}} );
391 Set( \$LogToFileNamed, 'rt.debug.log' );
395 sub set_config_wrapper {
398 my $old_sub = \&RT::Config::Set;
399 no warnings 'redefine';
400 *RT::Config::Set = sub {
401 # Determine if the caller is either from a test script, or
402 # from helper functions called by test script to alter
403 # configuration that should be written. This is necessary
404 # because some extensions (RTIR, for example) temporarily swap
405 # configuration values out and back in Mason during requests.
406 my @caller = caller(1); # preserve list context
407 @caller = caller(0) unless @caller;
409 if ( ($caller[1]||'') =~ /\.t$/) {
410 my ($self, $name) = @_;
411 my $type = $RT::Config::META{$name}->{'Type'} || 'SCALAR';
417 my $sigil = $sigils{$type} || $sigils{'SCALAR'};
418 open( my $fh, '<', $tmp{'config'}{'RT'} )
419 or die "Couldn't open config file: $!";
422 if (not @lines or /^Set\(/) {
430 # Traim trailing newlines and "1;"
431 $lines[-1] =~ s/(^1;\n|^\n)*\Z//m;
433 # Remove any previous definitions of this var
434 @lines = grep {not /^Set\(\s*\Q$sigil$name\E\b/} @lines;
436 # Format the new value for output
437 require Data::Dumper;
438 local $Data::Dumper::Terse = 1;
439 my $dump = Data::Dumper::Dumper([@_[2 .. $#_]]);
440 $dump =~ s/;?\s+\Z//;
441 push @lines, "Set( ${sigil}${name}, \@{". $dump ."});\n";
442 push @lines, "\n1;\n";
444 # Re-write the configuration file
445 open( $fh, '>', $tmp{'config'}{'RT'} )
446 or die "Couldn't open config file: $!";
447 print $fh $_ for @lines;
451 warn "you're changing config option in a test file"
452 ." when server is active";
455 return $old_sub->(@_);
460 my $builder = Test::More->builder;
461 binmode $builder->output, ":encoding(utf8)";
462 binmode $builder->failure_output, ":encoding(utf8)";
463 binmode $builder->todo_output, ":encoding(utf8)";
470 unless (defined $ENV{'RT_DBA_USER'} && defined $ENV{'RT_DBA_PASSWORD'}) {
471 Test::More::BAIL_OUT(
472 "RT_DBA_USER and RT_DBA_PASSWORD environment variables need"
473 ." to be set in order to run 'make test'"
474 ) unless $self->db_requires_no_dba;
478 if (my $forceopt = $ENV{RT_TEST_FORCE_OPT}) {
479 Test::More::diag "forcing $forceopt";
483 # Short-circuit the rest of ourselves if we don't want a db
489 my $db_type = RT->Config->Get('DatabaseType');
491 __reconnect_rt('as dba');
492 $RT::Handle->InsertSchema;
493 $RT::Handle->InsertACL unless $db_type eq 'Oracle';
498 $RT::Handle->InsertInitialData
499 unless $args{noinitialdata};
501 $RT::Handle->InsertData( $RT::EtcPath . "/initialdata" )
502 unless $args{noinitialdata} or $args{nodata};
504 $self->bootstrap_plugins_db( %args );
507 sub bootstrap_plugins_paths {
511 return unless $args{'plugins'};
512 my @plugins = @{ $args{'plugins'} };
515 if ( $args{'testing'} ) {
517 $cwd = Cwd::getcwd();
521 my $old_func = \&RT::Plugin::_BasePath;
522 no warnings 'redefine';
523 *RT::Plugin::_BasePath = sub {
524 my $name = $_[0]->{'name'};
526 return $cwd if $args{'testing'} && $name eq $args{'testing'};
528 if ( grep $name eq $_, @plugins ) {
529 my $variants = join "(?:|::|-|_)", map "\Q$_\E", split /::/, $name;
530 my ($path) = map $ENV{$_}, grep /^RT_TEST_PLUGIN_(?:$variants).*_ROOT$/i, keys %ENV;
531 return $path if $path;
533 return $old_func->(@_);
537 sub bootstrap_plugins_db {
541 return unless $args{'plugins'};
545 my @plugins = @{ $args{'plugins'} };
546 foreach my $name ( @plugins ) {
547 my $plugin = RT::Plugin->new( name => $name );
548 Test::More::diag( "Initializing DB for the $name plugin" )
549 if $ENV{'TEST_VERBOSE'};
551 my $etc_path = $plugin->Path('etc');
552 Test::More::diag( "etc path of the plugin is '$etc_path'" )
553 if $ENV{'TEST_VERBOSE'};
555 unless ( -e $etc_path ) {
556 # We can't tell if the plugin has no data, or we screwed up the etc/ path
557 Test::More::ok(1, "There is no etc dir: no schema" );
558 Test::More::ok(1, "There is no etc dir: no ACLs" );
559 Test::More::ok(1, "There is no etc dir: no data" );
563 __reconnect_rt('as dba');
566 my ($ret, $msg) = $RT::Handle->InsertSchema( undef, $etc_path );
567 Test::More::ok($ret || $msg =~ /^Couldn't find schema/, "Created schema: ".($msg||''));
571 my ($ret, $msg) = $RT::Handle->InsertACL( undef, $etc_path );
572 Test::More::ok($ret || $msg =~ /^Couldn't find ACLs/, "Created ACL: ".($msg||''));
576 my $data_file = File::Spec->catfile( $etc_path, 'initialdata' );
577 if ( -e $data_file ) {
579 my ($ret, $msg) = $RT::Handle->InsertData( $data_file );;
580 Test::More::ok($ret, "Inserted data".($msg||''));
582 Test::More::ok(1, "There is no data file" );
589 my ($dsn, $user, $pass) = @_;
590 if ( $dsn =~ /Oracle/i ) {
591 $ENV{'NLS_LANG'} = "AMERICAN_AMERICA.AL32UTF8";
592 $ENV{'NLS_NCHAR'} = "AL32UTF8";
594 my $dbh = DBI->connect(
596 { RaiseError => 0, PrintError => 1 },
599 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
600 print STDERR $msg; exit -1;
605 sub __create_database {
606 # bootstrap with dba cred
608 RT::Handle->SystemDSN,
609 $ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
612 unless ( $ENV{RT_TEST_PARALLEL} ) {
613 # already dropped db in parallel tests, need to do so for other cases.
614 __drop_database( $dbh );
617 RT::Handle->CreateDatabase( $dbh );
622 sub __drop_database {
625 # Pg doesn't like if you issue a DROP DATABASE while still connected
626 # it's still may fail if web-server is out there and holding a connection
629 my $my_dbh = $dbh? 0 : 1;
631 RT::Handle->SystemDSN,
632 $ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
635 # We ignore errors intentionally by not checking the return value of
636 # DropDatabase below, so let's also suppress DBI's printing of errors when
637 # we overzealously drop.
638 local $dbh->{PrintError} = 0;
639 local $dbh->{PrintWarn} = 0;
641 RT::Handle->DropDatabase( $dbh );
642 $dbh->disconnect if $my_dbh;
649 # look at %DBIHandle and $PrevHandle in DBIx::SB::Handle for explanation
650 $RT::Handle = RT::Handle->new;
651 $RT::Handle->dbh( undef );
652 $RT::Handle->Connect(
654 ? (User => $ENV{RT_DBA_USER}, Password => $ENV{RT_DBA_PASSWORD})
657 $RT::Handle->PrintError;
658 $RT::Handle->dbh->{PrintError} = 1;
659 return $RT::Handle->dbh;
662 sub __disconnect_rt {
663 # look at %DBIHandle and $PrevHandle in DBIx::SB::Handle for explanation
664 $RT::Handle->dbh->disconnect if $RT::Handle and $RT::Handle->dbh;
666 %DBIx::SearchBuilder::Handle::DBIHandle = ();
667 $DBIx::SearchBuilder::Handle::PrevHandle = undef;
671 delete $RT::System->{attributes};
673 DBIx::SearchBuilder::Record::Cachable->FlushCache
674 if DBIx::SearchBuilder::Record::Cachable->can("FlushCache");
680 # We use local to ensure that the $filter we grab is from InitLogging
681 # and not the handler generated by a previous call to this function
683 local $SIG{__WARN__};
685 $filter = $SIG{__WARN__};
687 $SIG{__WARN__} = sub {
688 $filter->(@_) if $filter;
689 # Avoid reporting this anonymous call frame as the source of the warning.
690 goto &$Test_NoWarnings_Catcher;
697 =head2 load_or_create_user
701 sub load_or_create_user {
703 my %args = ( Privileged => 1, Disabled => 0, @_ );
705 my $MemberOf = delete $args{'MemberOf'};
706 $MemberOf = [ $MemberOf ] if defined $MemberOf && !ref $MemberOf;
709 my $obj = RT::User->new( RT->SystemUser );
710 if ( $args{'Name'} ) {
711 $obj->LoadByCols( Name => $args{'Name'} );
712 } elsif ( $args{'EmailAddress'} ) {
713 $obj->LoadByCols( EmailAddress => $args{'EmailAddress'} );
715 die "Name or EmailAddress is required";
719 $obj->SetPrivileged( $args{'Privileged'} || 0 )
720 if ($args{'Privileged'}||0) != ($obj->Privileged||0);
721 $obj->SetDisabled( $args{'Disabled'} || 0 )
722 if ($args{'Disabled'}||0) != ($obj->Disabled||0);
724 my ($val, $msg) = $obj->Create( %args );
725 die "$msg" unless $val;
728 # clean group membership
730 require RT::GroupMembers;
731 my $gms = RT::GroupMembers->new( RT->SystemUser );
732 my $groups_alias = $gms->Join(
733 FIELD1 => 'GroupId', TABLE2 => 'Groups', FIELD2 => 'id',
736 ALIAS => $groups_alias, FIELD => 'Domain', VALUE => 'UserDefined',
739 $gms->Limit( FIELD => 'MemberId', VALUE => $obj->id );
740 while ( my $group_member_record = $gms->Next ) {
741 $group_member_record->Delete;
745 # add new user to groups
746 foreach ( @$MemberOf ) {
747 my $group = RT::Group->new( RT::SystemUser() );
748 $group->LoadUserDefinedGroup( $_ );
749 die "couldn't load group '$_'" unless $group->id;
750 $group->AddMember( $obj->id );
757 sub load_or_create_group {
762 my $group = RT::Group->new( RT->SystemUser );
763 $group->LoadUserDefinedGroup( $name );
764 unless ( $group->id ) {
765 my ($id, $msg) = $group->CreateUserDefinedGroup(
768 die "$msg" unless $id;
771 if ( $args{Members} ) {
772 my $cur = $group->MembersObj;
773 while ( my $entry = $cur->Next ) {
774 my ($status, $msg) = $entry->Delete;
775 die "$msg" unless $status;
778 foreach my $new ( @{ $args{Members} } ) {
779 my ($status, $msg) = $group->AddMember(
780 ref($new)? $new->id : $new,
782 die "$msg" unless $status;
789 =head2 load_or_create_queue
793 sub load_or_create_queue {
795 my %args = ( Disabled => 0, @_ );
796 my $obj = RT::Queue->new( RT->SystemUser );
797 if ( $args{'Name'} ) {
798 $obj->LoadByCols( Name => $args{'Name'} );
800 die "Name is required";
802 unless ( $obj->id ) {
803 my ($val, $msg) = $obj->Create( %args );
804 die "$msg" unless $val;
806 my @fields = qw(CorrespondAddress CommentAddress);
807 foreach my $field ( @fields ) {
808 next unless exists $args{ $field };
809 next if $args{ $field } eq ($obj->$field || '');
811 no warnings 'uninitialized';
812 my $method = 'Set'. $field;
813 my ($val, $msg) = $obj->$method( $args{ $field } );
814 die "$msg" unless $val;
821 sub delete_queue_watchers {
825 foreach my $q ( @queues ) {
826 foreach my $t (qw(Cc AdminCc) ) {
827 $q->DeleteWatcher( Type => $t, PrincipalId => $_->MemberId )
828 foreach @{ $q->$t()->MembersObj->ItemsArrayRef };
834 local $Test::Builder::Level = $Test::Builder::Level + 1;
837 my $defaults = shift;
839 @data = sort { rand(100) <=> rand(100) } @data
840 if delete $defaults->{'RandomOrder'};
842 $defaults->{'Queue'} ||= 'General';
846 my %args = %{ shift @data };
847 $args{$_} = $res[ $args{$_} ]->id foreach
848 grep $args{ $_ }, keys %RT::Link::TYPEMAP;
849 push @res, $self->create_ticket( %$defaults, %args );
855 local $Test::Builder::Level = $Test::Builder::Level + 1;
860 if ( blessed $args{'Queue'} ) {
861 $args{Queue} = $args{'Queue'}->id;
863 elsif ($args{Queue} && $args{Queue} =~ /\D/) {
864 my $queue = RT::Queue->new(RT->SystemUser);
865 if (my $id = $queue->Load($args{Queue}) ) {
868 die ("Error: Invalid queue $args{Queue}");
872 if ( my $content = delete $args{'Content'} ) {
873 $args{'MIMEObj'} = MIME::Entity->build(
874 From => Encode::encode( "UTF-8", $args{'Requestor'} ),
875 Subject => RT::Interface::Email::EncodeToMIME( String => $args{'Subject'} ),
876 Type => "text/plain",
878 Data => Encode::encode( "UTF-8", $content ),
882 if ( my $cfs = delete $args{'CustomFields'} ) {
883 my $q = RT::Queue->new( RT->SystemUser );
884 $q->Load( $args{'Queue'} );
885 while ( my ($k, $v) = each %$cfs ) {
886 my $cf = $q->CustomField( $k );
888 RT->Logger->error("Couldn't load custom field $k");
892 $args{'CustomField-'. $cf->id} = $v;
896 my $ticket = RT::Ticket->new( RT->SystemUser );
897 my ( $id, undef, $msg ) = $ticket->Create( %args );
898 Test::More::ok( $id, "ticket created" )
899 or Test::More::diag("error: $msg");
901 # hackish, but simpler
902 if ( $args{'LastUpdatedBy'} ) {
903 $ticket->__Set( Field => 'LastUpdatedBy', Value => $args{'LastUpdatedBy'} );
907 for my $field ( keys %args ) {
908 #TODO check links and watchers
910 if ( $field =~ /CustomField-(\d+)/ ) {
912 my $got = join ',', sort map $_->Content,
913 @{ $ticket->CustomFieldValues($cf)->ItemsArrayRef };
914 my $expected = ref $args{$field}
915 ? join( ',', sort @{ $args{$field} } )
917 Test::More::is( $got, $expected, 'correct CF values' );
920 next if ref $args{$field};
921 next unless $ticket->can($field) or $ticket->_Accessible($field,"read");
922 next if ref $ticket->$field();
923 Test::More::is( $ticket->$field(), $args{$field}, "$field is correct" );
933 my $tickets = RT::Tickets->new( RT->SystemUser );
935 $tickets->FromSQL( $query );
940 while ( my $ticket = $tickets->Next ) {
945 =head2 load_or_create_custom_field
949 sub load_or_create_custom_field {
951 my %args = ( Disabled => 0, @_ );
952 my $obj = RT::CustomField->new( RT->SystemUser );
953 if ( $args{'Name'} ) {
955 Name => $args{'Name'},
956 LookupType => RT::Ticket->CustomFieldLookupType,
957 ObjectId => $args{'Queue'},
960 die "Name is required";
962 unless ( $obj->id ) {
963 my ($val, $msg) = $obj->Create( %args );
964 die "$msg" unless $val;
973 $current = $current ? RT::CurrentUser->new($current) : RT->SystemUser;
974 my $tickets = RT::Tickets->new( $current );
975 $tickets->OrderBy( FIELD => 'id', ORDER => 'DESC' );
976 $tickets->Limit( FIELD => 'id', OPERATOR => '>', VALUE => '0' );
977 $tickets->RowsPerPage( 1 );
978 return $tickets->First;
986 RT::ACE->new( RT->SystemUser );
987 my @fields = keys %{ RT::ACE->_ClassAccessible };
990 my $acl = RT::ACL->new( RT->SystemUser );
991 $acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
994 while ( my $ace = $acl->Next ) {
995 my $obj = $ace->PrincipalObj->Object;
996 if ( $obj->isa('RT::Group') && $obj->Domain eq 'ACLEquivalence' && $obj->Instance == RT->Nobody->id ) {
1001 foreach my $field( @fields ) {
1002 $tmp{ $field } = $ace->__Value( $field );
1009 sub restore_rights {
1012 foreach my $entry ( @entries ) {
1013 my $ace = RT::ACE->new( RT->SystemUser );
1014 my ($status, $msg) = $ace->RT::Record::Create( %$entry );
1015 unless ( $status ) {
1016 Test::More::diag "couldn't create a record: $msg";
1025 my $acl = RT::ACL->new( RT->SystemUser );
1026 $acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
1027 while ( my $ace = $acl->Next ) {
1028 my $obj = $ace->PrincipalObj->Object;
1029 if ( $obj->isa('RT::Group') && $obj->Domain eq 'ACLEquivalence' && $obj->Instance == RT->Nobody->id ) {
1034 return $self->add_rights( @_ );
1039 my @list = ref $_[0]? @_: @_? { @_ }: ();
1042 foreach my $e (@list) {
1043 my $principal = delete $e->{'Principal'};
1044 unless ( ref $principal ) {
1045 if ( $principal =~ /^(everyone|(?:un)?privileged)$/i ) {
1046 $principal = RT::Group->new( RT->SystemUser );
1047 $principal->LoadSystemInternalGroup($1);
1049 my $type = $principal;
1050 $principal = RT::Group->new( RT->SystemUser );
1051 $principal->LoadRoleGroup(
1052 Object => ($e->{'Object'} || RT->System),
1056 die "Principal is not an object nor the name of a system or role group"
1057 unless $principal->id;
1059 unless ( $principal->isa('RT::Principal') ) {
1060 if ( $principal->can('PrincipalObj') ) {
1061 $principal = $principal->PrincipalObj;
1064 my @rights = ref $e->{'Right'}? @{ $e->{'Right'} }: ($e->{'Right'});
1065 foreach my $right ( @rights ) {
1066 my ($status, $msg) = $principal->GrantRight( %$e, Right => $right );
1067 $RT::Logger->debug($msg);
1073 =head2 switch_templates_to TYPE
1075 This runs etc/upgrade/switch-templates-to in order to change the templates from
1076 HTML to text or vice versa. TYPE is the type to switch to, either C<html> or
1081 sub switch_templates_to {
1085 return $self->run_and_capture(
1086 command => "$RT::EtcPath/upgrade/switch-templates-to",
1091 =head2 switch_templates_ok TYPE
1093 Calls L<switch_template_to> and tests the return values.
1097 sub switch_templates_ok {
1101 my ($exit, $output) = $self->switch_templates_to($type);
1104 Test::More::fail("Switched templates to $type cleanly");
1105 diag("**** etc/upgrade/switch-templates-to exited with ".($exit >> 8).":\n$output");
1107 Test::More::pass("Switched templates to $type cleanly");
1110 return ($exit, $output);
1116 require RT::Test::Web;
1118 url => RT::Test::Web->rt_base_url,
1120 action => 'correspond',
1123 command => $RT::BinPath .'/rt-mailgate',
1126 my $message = delete $args{'message'};
1128 $args{after_open} = sub {
1129 my $child_in = shift;
1130 if ( UNIVERSAL::isa($message, 'MIME::Entity') ) {
1131 $message->print( $child_in );
1133 print $child_in $message;
1137 $self->run_and_capture(%args);
1140 sub run_and_capture {
1144 my $after_open = delete $args{after_open};
1146 my $cmd = delete $args{'command'};
1147 die "Couldn't find command ($cmd)" unless -f $cmd;
1149 $cmd .= ' --debug' if delete $args{'debug'};
1151 my $args = delete $args{'args'};
1153 while( my ($k,$v) = each %args ) {
1155 $cmd .= " --$k '$v'";
1157 $cmd .= " $args" if defined $args;
1160 DBIx::SearchBuilder::Record::Cachable->FlushCache;
1163 my ($child_out, $child_in);
1164 my $pid = IPC::Open2::open2($child_out, $child_in, $cmd);
1166 $after_open->($child_in, $child_out) if $after_open;
1170 my $result = do { local $/; <$child_out> };
1173 return ($?, $result);
1176 sub send_via_mailgate_and_http {
1178 my $message = shift;
1181 my ($status, $gate_result) = $self->run_mailgate(
1182 message => $message, %args
1186 unless ( $status >> 8 ) {
1187 ($id) = ($gate_result =~ /Ticket:\s*(\d+)/i);
1189 Test::More::diag "Couldn't find ticket id in text:\n$gate_result"
1190 if $ENV{'TEST_VERBOSE'};
1193 Test::More::diag "Mailgate output:\n$gate_result"
1194 if $ENV{'TEST_VERBOSE'};
1196 return ($status, $id);
1200 sub send_via_mailgate {
1202 my $message = shift;
1203 my %args = ( action => 'correspond',
1208 if ( UNIVERSAL::isa( $message, 'MIME::Entity' ) ) {
1209 $message = $message->as_string;
1212 my ( $status, $error_message, $ticket )
1213 = RT::Interface::Email::Gateway( {%args, message => $message} );
1214 return ( $status, $ticket ? $ticket->id : 0 );
1219 sub open_mailgate_ok {
1220 local $Test::Builder::Level = $Test::Builder::Level + 1;
1222 my $baseurl = shift;
1223 my $queue = shift || 'general';
1224 my $action = shift || 'correspond';
1225 Test::More::ok(open(my $mail, '|-', "$RT::BinPath/rt-mailgate --url $baseurl --queue $queue --action $action"), "Opened the mailgate - $!");
1230 sub close_mailgate_ok {
1231 local $Test::Builder::Level = $Test::Builder::Level + 1;
1235 Test::More::is ($? >> 8, 0, "The mail gateway exited normally. yay");
1239 local $Test::Builder::Level = $Test::Builder::Level + 1;
1241 my $expected = shift;
1243 my $mailsent = scalar grep /\S/, split /%% split me! %%\n/,
1244 RT::Test->file_content(
1251 $mailsent, $expected,
1252 "The number of mail sent ($expected) matches. yay"
1256 sub fetch_caught_mails {
1258 return grep /\S/, split /%% split me! %%\n/,
1259 RT::Test->file_content(
1266 sub clean_caught_mails {
1267 unlink $tmp{'mailbox'};
1269 my $validator_path = "$RT::SbinPath/rt-validator";
1272 my %args = (check => 1, resolve => 0, force => 1, @_ );
1274 my $cmd = $validator_path;
1275 die "Couldn't find $cmd command" unless -f $cmd;
1277 while( my ($k,$v) = each %args ) {
1279 $cmd .= " --$k '$v'";
1284 my ($child_out, $child_in);
1285 my $pid = IPC::Open2::open2($child_out, $child_in, $cmd);
1288 my $result = do { local $/; <$child_out> };
1292 DBIx::SearchBuilder::Record::Cachable->FlushCache
1293 if $args{'resolve'};
1295 return ($?, $result);
1299 local $Test::Builder::Level = $Test::Builder::Level + 1;
1302 my ($ecode, $res) = $self->run_validator;
1303 Test::More::is( $ecode, 0, 'no invalid records' )
1304 or Test::More::diag "errors:\n$res";
1307 =head2 object_scrips_are
1309 Takes an L<RT::Scrip> object or ID as the first argument and an arrayref of
1310 L<RT::Queue> objects and/or Queue IDs as the second argument.
1312 The scrip's applications (L<RT::ObjectScrip> records) are tested to ensure they
1313 exactly match the arrayref.
1315 An optional third arrayref may be passed to enumerate and test the queues the
1316 scrip is B<not> added to. This is most useful for testing the API returns the
1321 sub object_scrips_are {
1322 local $Test::Builder::Level = $Test::Builder::Level + 1;
1325 my $to = shift || [];
1328 unless (blessed($scrip)) {
1330 $scrip = RT::Scrip->new( RT->SystemUser );
1334 $to = [ map { blessed($_) ? $_->id : $_ } @$to ];
1335 Test::More::ok($scrip->IsAdded($_), "added to queue $_" ) foreach @$to;
1336 Test::More::is_deeply(
1337 [sort map $_->id, @{ $scrip->AddedTo->ItemsArrayRef }],
1338 [sort grep $_, @$to ],
1339 'correct list of added to queues',
1343 $not_to = [ map { blessed($_) ? $_->id : $_ } @$not_to ];
1344 Test::More::ok(!$scrip->IsAdded($_), "not added to queue $_" ) foreach @$not_to;
1345 Test::More::is_deeply(
1346 [sort map $_->id, @{ $scrip->NotAddedTo->ItemsArrayRef }],
1347 [sort grep $_, @$not_to ],
1348 'correct list of not added to queues',
1353 =head2 get_relocatable_dir
1355 Takes a path relative to the location of the test file that is being
1356 run and returns a path that takes the invocation path into account.
1358 e.g. C<RT::Test::get_relocatable_dir(File::Spec->updir(), 'data', 'emails')>
1360 Parent directory traversals (C<..> or File::Spec->updir()) are naively
1361 canonicalized based on the test file path (C<$0>) so that symlinks aren't
1362 followed. This is the exact opposite behaviour of most filesystems and is
1363 considered "wrong", however it is necessary for some subsets of tests which are
1364 symlinked into the testing tree.
1368 sub get_relocatable_dir {
1369 my @directories = File::Spec->splitdir(
1370 File::Spec->rel2abs((File::Spec->splitpath($0))[1])
1372 push @directories, File::Spec->splitdir($_) for @_;
1375 for (@directories) {
1376 if ($_ eq "..") { pop @clean }
1377 elsif ($_ ne ".") { push @clean, $_ }
1379 return File::Spec->catdir(@clean);
1382 =head2 get_relocatable_file
1384 Same as get_relocatable_dir, but takes a file and a path instead
1387 e.g. RT::Test::get_relocatable_file('test-email',
1388 (File::Spec->updir(), 'data', 'emails'))
1392 sub get_relocatable_file {
1394 return File::Spec->catfile(get_relocatable_dir(@_), $file);
1397 sub find_relocatable_path {
1400 # A simple strategy to find e.g., t/data/gnupg/keys, from the dir
1401 # where test file lives. We try up to 3 directories up
1402 my $path = File::Spec->catfile( @path );
1403 for my $up ( 0 .. 2 ) {
1404 my $p = get_relocatable_dir($path);
1407 $path = File::Spec->catfile( File::Spec->updir(), $path );
1412 sub get_abs_relocatable_dir {
1413 (my $volume, my $directories, my $file) = File::Spec->splitpath($0);
1414 if (File::Spec->file_name_is_absolute($directories)) {
1415 return File::Spec->catdir($directories, @_);
1417 return File::Spec->catdir(Cwd->getcwd(), $directories, @_);
1424 DIR => $tmp{directory},
1429 sub import_gnupg_key {
1432 my $type = shift || 'secret';
1434 $key =~ s/\@/-at-/g;
1435 $key .= ".$type.key";
1437 my $path = find_relocatable_path( 'data', 'gnupg', 'keys' );
1439 die "can't find the dir where gnupg keys are stored"
1442 return RT::Crypt::GnuPG->ImportKey(
1443 RT::Test->file_content( [ $path, $key ] ) );
1446 sub lsign_gnupg_key {
1450 return RT::Crypt::GnuPG->CallGnuPG(
1451 Command => '--lsign-key',
1452 CommandArgs => [$key],
1455 while ( my $str = readline $handle{'status'} ) {
1456 if ( $str =~ /^\[GNUPG:\]\s*GET_BOOL sign_uid\..*/ ) {
1457 print { $handle{'command'} } "y\n";
1464 sub trust_gnupg_key {
1468 return RT::Crypt::GnuPG->CallGnuPG(
1469 Command => '--edit-key',
1470 CommandArgs => [$key],
1474 while ( my $str = readline $handle{'status'} ) {
1475 if ( $str =~ /^\[GNUPG:\]\s*\QGET_LINE keyedit.prompt/ ) {
1477 print { $handle{'command'} } "quit\n";
1479 print { $handle{'command'} } "trust\n";
1481 } elsif ( $str =~ /^\[GNUPG:\]\s*\QGET_LINE edit_ownertrust.value/ ) {
1482 print { $handle{'command'} } "5\n";
1483 } elsif ( $str =~ /^\[GNUPG:\]\s*\QGET_BOOL edit_ownertrust.set_ultimate.okay/ ) {
1484 print { $handle{'command'} } "y\n";
1495 require RT::Test::Web;
1497 if ($rttest_opt{nodb} and not $rttest_opt{server_ok}) {
1498 die "You are trying to use a test web server without a database. "
1499 ."You may want noinitialdata => 1 instead. "
1500 ."Pass server_ok => 1 if you know what you're doing.";
1504 $ENV{'RT_TEST_WEB_HANDLER'} = undef
1505 if $rttest_opt{actual_server} && ($ENV{'RT_TEST_WEB_HANDLER'}||'') eq 'inline';
1506 $ENV{'RT_TEST_WEB_HANDLER'} ||= 'plack';
1507 my $which = $ENV{'RT_TEST_WEB_HANDLER'};
1508 my ($server, $variant) = split /\+/, $which, 2;
1510 my $function = 'start_'. $server .'_server';
1511 unless ( $self->can($function) ) {
1512 die "Don't know how to start server '$server'";
1514 return $self->$function( variant => $variant, @_ );
1519 my %server_opt = @_;
1524 open( my $warn_fh, ">", \$warnings );
1525 local *STDERR = $warn_fh;
1527 if ($server_opt{variant} and $server_opt{variant} eq 'rt-server') {
1529 my $file = "$RT::SbinPath/rt-server";
1530 my $psgi = do $file;
1532 die "Couldn't parse $file: $@" if $@;
1533 die "Couldn't do $file: $!" unless defined $psgi;
1534 die "Couldn't run $file" unless $psgi;
1539 require RT::Interface::Web::Handler;
1540 $app = RT::Interface::Web::Handler->PSGIApp;
1543 require Plack::Middleware::Test::StashWarnings;
1544 my $stashwarnings = Plack::Middleware::Test::StashWarnings->new(
1545 $ENV{'RT_TEST_WEB_HANDLER'} && $ENV{'RT_TEST_WEB_HANDLER'} eq 'inline' ? ( verbose => 0 ) : () );
1546 $app = $stashwarnings->wrap($app);
1548 if ($server_opt{basic_auth}) {
1549 require Plack::Middleware::Auth::Basic;
1550 $app = Plack::Middleware::Auth::Basic->wrap(
1552 authenticator => $server_opt{basic_auth} eq 'anon' ? sub { 1 } : sub {
1553 my ($username, $password) = @_;
1554 return $username eq 'root' && $password eq 'password';
1560 $stashwarnings->add_warning( $warnings ) if $warnings;
1565 sub start_plack_server {
1566 local $Test::Builder::Level = $Test::Builder::Level + 1;
1569 require Plack::Loader;
1570 my $plack_server = Plack::Loader->load
1573 server_ready => sub {
1574 kill 'USR1' => getppid();
1577 # We are expecting a USR1 from the child process after it's ready
1578 # to listen. We set this up _before_ we fork to avoid race
1581 local $SIG{USR1} = sub { $handled = 1};
1585 die "failed to fork" unless defined $pid;
1588 sleep 15 unless $handled;
1589 Test::More::diag "did not get expected USR1 for test server readiness"
1591 push @SERVERS, $pid;
1592 my $Tester = Test::Builder->new;
1593 $Tester->ok(1, "started plack server ok");
1596 unless $rttest_opt{nodb};
1597 return ("http://localhost:$port", RT::Test::Web->new);
1602 or die "Can't start a new session: $!";
1604 # stick this in a scope so that when $app is garbage collected,
1605 # StashWarnings can complain about unhandled warnings
1607 $plack_server->run($self->test_app(@_));
1614 sub start_inline_server {
1615 local $Test::Builder::Level = $Test::Builder::Level + 1;
1618 require Test::WWW::Mechanize::PSGI;
1619 unshift @RT::Test::Web::ISA, 'Test::WWW::Mechanize::PSGI';
1621 # Clear out squished CSS and JS cache, since it's retained across
1622 # servers, since it's in-process
1623 RT::Interface::Web->ClearSquished;
1624 require RT::Interface::Web::Request;
1625 RT::Interface::Web::Request->clear_callback_cache;
1627 Test::More::ok(1, "psgi test server ok");
1628 $TEST_APP = $self->test_app(@_);
1629 return ("http://localhost:$port", RT::Test::Web->new);
1632 sub start_apache_server {
1633 local $Test::Builder::Level = $Test::Builder::Level + 1;
1635 my %server_opt = @_;
1636 $server_opt{variant} ||= 'mod_perl';
1637 $ENV{RT_TEST_WEB_HANDLER} = "apache+$server_opt{variant}";
1639 require RT::Test::Apache;
1640 my $pid = RT::Test::Apache->start_server(
1645 push @SERVERS, $pid;
1647 my $url = RT->Config->Get('WebURL');
1649 return ($url, RT::Test::Web->new);
1655 return unless @SERVERS;
1657 kill 'TERM', @SERVERS;
1658 foreach my $pid (@SERVERS) {
1659 if ($ENV{RT_TEST_WEB_HANDLER} =~ /^apache/) {
1660 sleep 1 while kill 0, $pid;
1669 sub temp_directory {
1670 return $tmp{'directory'};
1678 $path = File::Spec->catfile( @$path ) if ref $path eq 'ARRAY';
1680 open( my $fh, "<:raw", $path )
1682 warn "couldn't open file '$path': $!" unless $args{noexist};
1685 my $content = do { local $/; <$fh> };
1688 unlink $path if $args{'unlink'};
1693 sub find_executable {
1696 return File::Which::which( @_ );
1700 return unless $ENV{RT_TEST_VERBOSE} || $ENV{TEST_VERBOSE};
1701 goto \&Test::More::diag;
1706 require RT::EmailParser;
1707 my $parser = RT::EmailParser->new;
1708 $parser->ParseMIMEEntityFromScalar( $mail );
1709 return $parser->Entity;
1713 Test::More::ok($_[0], $_[1] || 'This works');
1717 Test::More::ok(!$_[0], $_[1] || 'This should fail');
1721 my ($cmd, @args) = @_;
1722 my $builder = RT::Test->builder;
1724 if ($cmd eq "skip_all") {
1725 $check_warnings_in_end = 0;
1726 } elsif ($cmd eq "tests") {
1727 # Increment the test count for the warnings check
1730 $builder->plan($cmd, @args);
1734 my $builder = RT::Test->builder;
1736 Test::NoWarnings::had_no_warnings();
1737 $check_warnings_in_end = 0;
1739 $builder->done_testing(@_);
1743 my $Test = RT::Test->builder;
1744 return if $Test->{Original_Pid} != $$;
1746 # we are in END block and should protect our exit code
1747 # so calls below may call system or kill that clobbers $?
1750 Test::NoWarnings::had_no_warnings() if $check_warnings_in_end;
1752 RT::Test->stop_server(1);
1755 if ( !$Test->is_passing ) {
1756 $tmp{'directory'}->unlink_on_destroy(0);
1759 "Some tests failed or we bailed out, tmp directory"
1760 ." '$tmp{directory}' is not cleaned"
1764 if ( $ENV{RT_TEST_PARALLEL} && $created_new_db ) {
1768 # Drop our port from t/tmp/ports; do this after dropping the
1769 # database, as our port lock is also a lock on the database name.
1772 my $portfile = "$tmp{'directory'}/../ports";
1773 sysopen(PORTS, $portfile, O_RDWR|O_CREAT)
1774 or die "Can't write to ports file $portfile: $!";
1775 flock(PORTS, LOCK_EX)
1776 or die "Can't write-lock ports file $portfile: $!";
1777 $ports{$_}++ for split ' ', join("",<PORTS>);
1778 delete $ports{$port};
1781 print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
1782 close(PORTS) or die "Can't close ports file: $!";
1787 # ease the used only once warning
1790 %{'RT::I18N::en_us::Lexicon'};
1791 %{'Win32::Locale::Lexicon'};