1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 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 }}}
55 use base 'Test::More';
57 # We use the Test::NoWarnings catching and reporting functionality, but need to
58 # wrap it in our own special handler because of the warn handler installed via
60 require Test::NoWarnings;
62 my $Test_NoWarnings_Catcher = $SIG{__WARN__};
63 my $check_warnings_in_end = 1;
66 use File::Temp qw(tempfile);
67 use File::Path qw(mkpath);
70 our @EXPORT = qw(is_empty diag parse_mail works fails plan done_testing);
91 To run the rt test suite with coverage support, install L<Devel::Cover> and run:
93 make test RT_DBA_USER=.. RT_DBA_PASSWORD=.. HARNESS_PERL_SWITCHES=-MDevel::Cover
94 cover -ignore_re '^var/mason_data/' -ignore_re '^t/'
96 The coverage tests have DevelMode turned off, and have
97 C<named_component_subs> enabled for L<HTML::Mason> to avoid an optimizer
98 problem in Perl that hides the top-level optree from L<Devel::Cover>.
106 delete $ENV{$_} for qw/LANGUAGE LC_ALL LC_MESSAGES LANG/;
112 my %args = %rttest_opt = @_;
114 $rttest_opt{'nodb'} = $args{'nodb'} = 1 if $^C;
116 # Spit out a plan (if we got one) *before* we load modules
117 if ( $args{'tests'} ) {
118 plan( tests => $args{'tests'} )
119 unless $args{'tests'} eq 'no_declare';
121 elsif ( exists $args{'tests'} ) {
122 # do nothing if they say "tests => undef" - let them make the plan
124 elsif ( $args{'skip_all'} ) {
125 plan(skip_all => $args{'skip_all'});
128 $class->builder->no_plan unless $class->builder->has_plan;
131 push @{ $args{'plugins'} ||= [] }, @{ $args{'requires'} }
132 if $args{'requires'};
133 push @{ $args{'plugins'} ||= [] }, $args{'testing'}
136 $class->bootstrap_tempdir;
138 $class->bootstrap_port;
140 $class->bootstrap_plugins_paths( %args );
142 $class->bootstrap_config( %args );
147 if (RT->Config->Get('DevelMode')) { require Module::Refresh; }
149 RT::InitPluginPaths();
152 $class->bootstrap_db( %args );
162 RT->Config->PostLoadCheck;
164 $class->set_config_wrapper;
166 my $screen_logger = $RT::Logger->remove( 'screen' );
167 require Log::Dispatch::Perl;
168 $RT::Logger->add( Log::Dispatch::Perl->new
170 min_level => $screen_logger->min_level,
171 action => { error => 'warn',
172 critical => 'warn' } ) );
174 # XXX: this should really be totally isolated environment so we
175 # can parallelize and be sane
176 mkpath [ $RT::MasonSessionDir ]
177 if RT->Config->Get('DatabaseType');
180 while ( my ($package) = caller($level-1) ) {
181 last unless $package =~ /Test/;
185 Test::More->export_to_level($level);
186 Test::NoWarnings->export_to_level($level);
188 # Blow away symbols we redefine to avoid warnings.
189 # better than "no warnings 'redefine'" because we might accidentally
190 # suppress a mistaken redefinition
192 delete ${ caller($level) . '::' }{diag};
193 delete ${ caller($level) . '::' }{plan};
194 delete ${ caller($level) . '::' }{done_testing};
195 __PACKAGE__->export_to_level($level);
200 local $Test::Builder::Level = $Test::Builder::Level + 1;
201 return Test::More::ok(1, $d) unless defined $v;
202 return Test::More::ok(1, $d) unless length $v;
203 return Test::More::is($v, '', $d);
206 my $created_new_db; # have we created new db? mainly for parallel testing
208 sub db_requires_no_dba {
210 my $db_type = RT->Config->Get('DatabaseType');
211 return 1 if $db_type eq 'SQLite';
219 # Determine which ports are in use
220 use Fcntl qw(:DEFAULT :flock);
221 my $portfile = "$tmp{'directory'}/../ports";
222 sysopen(PORTS, $portfile, O_RDWR|O_CREAT)
223 or die "Can't write to ports file $portfile: $!";
224 flock(PORTS, LOCK_EX)
225 or die "Can't write-lock ports file $portfile: $!";
226 $ports{$_}++ for split ' ', join("",<PORTS>);
228 # Pick a random port, checking that the port isn't in our in-use
229 # list, and that something isn't already listening there.
231 $port = 1024 + int rand(10_000) + $$ % 1024;
232 redo if $ports{$port};
234 # There is a race condition in here, where some non-RT::Test
235 # process claims the port after we check here but before our
236 # server binds. However, since we mostly care about race
237 # conditions with ourselves under high concurrency, this is
238 # generally good enough.
239 my $paddr = sockaddr_in( $port, inet_aton('localhost') );
240 socket( SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp') )
242 if ( connect( SOCK, $paddr ) ) {
251 # Write back out the in-use ports
254 print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
255 close(PORTS) or die "Can't close ports file: $!";
258 sub bootstrap_tempdir {
260 my ($test_dir, $test_file) = ('t', '');
262 if (File::Spec->rel2abs($0) =~ m{(?:^|[\\/])(x?t)[/\\](.*)}) {
265 $test_file =~ s{[/\\]}{-}g;
268 my $dir_name = File::Spec->rel2abs("$test_dir/tmp");
270 return $tmp{'directory'} = File::Temp->newdir(
271 "${test_file}XXXXXXXX",
276 sub bootstrap_config {
280 $tmp{'config'}{'RT'} = File::Spec->catfile(
281 "$tmp{'directory'}", 'RT_SiteConfig.pm'
283 open( my $config, '>', $tmp{'config'}{'RT'} )
284 or die "Couldn't open $tmp{'config'}{'RT'}: $!";
286 my $dbname = $ENV{RT_TEST_PARALLEL}? "rt4test_$port" : "rt4test";
288 Set( \$WebDomain, "localhost");
289 Set( \$WebPort, $port);
291 Set( \@LexiconLanguages, qw(en zh_TW fr ja));
292 Set( \$RTAddressRegexp , qr/^bad_re_that_doesnt_match\$/i);
294 if ( $ENV{'RT_TEST_DB_SID'} ) { # oracle case
295 print $config "Set( \$DatabaseName , '$ENV{'RT_TEST_DB_SID'}' );\n";
296 print $config "Set( \$DatabaseUser , '$dbname');\n";
298 print $config "Set( \$DatabaseName , '$dbname');\n";
299 print $config "Set( \$DatabaseUser , 'u${dbname}');\n";
302 if ( $args{'plugins'} ) {
303 print $config "Set( \@Plugins, qw(". join( ' ', @{ $args{'plugins'} } ) .") );\n";
305 my $plugin_data = File::Spec->rel2abs("t/data/plugins");
306 print $config qq[\$RT::PluginPath = "$plugin_data";\n];
309 if ( $INC{'Devel/Cover.pm'} ) {
310 print $config "Set( \$DevelMode, 0 );\n";
312 elsif ( $ENV{RT_TEST_DEVEL} ) {
313 print $config "Set( \$DevelMode, 1 );\n";
316 print $config "Set( \$DevelMode, 0 );\n";
319 $self->bootstrap_logging( $config );
322 my $mail_catcher = $tmp{'mailbox'} = File::Spec->catfile(
323 $tmp{'directory'}->dirname, 'mailbox.eml'
326 Set( \$MailCommand, sub {
329 open( my \$handle, '>>', '$mail_catcher' )
330 or die "Unable to open '$mail_catcher' for appending: \$!";
332 \$MIME->print(\$handle);
333 print \$handle "%% split me! %%\n";
338 $self->bootstrap_more_config($config, \%args);
340 print $config $args{'config'} if $args{'config'};
342 print $config "\n1;\n";
343 $ENV{'RT_SITE_CONFIG'} = $tmp{'config'}{'RT'};
349 sub bootstrap_more_config { }
351 sub bootstrap_logging {
355 # prepare file for logging
356 $tmp{'log'}{'RT'} = File::Spec->catfile(
357 "$tmp{'directory'}", 'rt.debug.log'
359 open( my $fh, '>', $tmp{'log'}{'RT'} )
360 or die "Couldn't open $tmp{'config'}{'RT'}: $!";
361 # make world writable so apache under different user
363 chmod 0666, $tmp{'log'}{'RT'};
366 Set( \$LogToSyslog , undef);
367 Set( \$LogToScreen , "warning");
368 Set( \$LogToFile, 'debug' );
369 Set( \$LogDir, q{$tmp{'directory'}} );
370 Set( \$LogToFileNamed, 'rt.debug.log' );
374 sub set_config_wrapper {
377 my $old_sub = \&RT::Config::Set;
378 no warnings 'redefine';
379 *RT::Config::Set = sub {
380 # Determine if the caller is either from a test script, or
381 # from helper functions called by test script to alter
382 # configuration that should be written. This is necessary
383 # because some extensions (RTIR, for example) temporarily swap
384 # configuration values out and back in Mason during requests.
385 my @caller = caller(1); # preserve list context
386 @caller = caller(0) unless @caller;
388 if ( ($caller[1]||'') =~ /\.t$/) {
389 my ($self, $name) = @_;
390 my $type = $RT::Config::META{$name}->{'Type'} || 'SCALAR';
396 my $sigil = $sigils{$type} || $sigils{'SCALAR'};
397 open( my $fh, '>>', $tmp{'config'}{'RT'} )
398 or die "Couldn't open config file: $!";
399 require Data::Dumper;
400 local $Data::Dumper::Terse = 1;
401 my $dump = Data::Dumper::Dumper([@_[2 .. $#_]]);
404 "\nSet(${sigil}${name}, \@{". $dump ."});\n1;\n";
408 warn "you're changing config option in a test file"
409 ." when server is active";
412 return $old_sub->(@_);
420 unless (defined $ENV{'RT_DBA_USER'} && defined $ENV{'RT_DBA_PASSWORD'}) {
421 Test::More::BAIL_OUT(
422 "RT_DBA_USER and RT_DBA_PASSWORD environment variables need"
423 ." to be set in order to run 'make test'"
424 ) unless $self->db_requires_no_dba;
428 if (my $forceopt = $ENV{RT_TEST_FORCE_OPT}) {
429 Test::More::diag "forcing $forceopt";
433 # Short-circuit the rest of ourselves if we don't want a db
439 my $db_type = RT->Config->Get('DatabaseType');
441 __reconnect_rt('as dba');
442 $RT::Handle->InsertSchema;
443 $RT::Handle->InsertACL unless $db_type eq 'Oracle';
448 $RT::Handle->InsertInitialData
449 unless $args{noinitialdata};
451 $RT::Handle->InsertData( $RT::EtcPath . "/initialdata" )
452 unless $args{noinitialdata} or $args{nodata};
454 $self->bootstrap_plugins_db( %args );
457 sub bootstrap_plugins_paths {
461 return unless $args{'plugins'};
462 my @plugins = @{ $args{'plugins'} };
465 if ( $args{'testing'} ) {
467 $cwd = Cwd::getcwd();
471 my $old_func = \&RT::Plugin::_BasePath;
472 no warnings 'redefine';
473 *RT::Plugin::_BasePath = sub {
474 my $name = $_[0]->{'name'};
476 return $cwd if $args{'testing'} && $name eq $args{'testing'};
478 if ( grep $name eq $_, @plugins ) {
479 my $variants = join "(?:|::|-|_)", map "\Q$_\E", split /::/, $name;
480 my ($path) = map $ENV{$_}, grep /^CHIMPS_(?:$variants).*_ROOT$/i, keys %ENV;
481 return $path if $path;
483 return $old_func->(@_);
487 sub bootstrap_plugins_db {
491 return unless $args{'plugins'};
495 my @plugins = @{ $args{'plugins'} };
496 foreach my $name ( @plugins ) {
497 my $plugin = RT::Plugin->new( name => $name );
498 Test::More::diag( "Initializing DB for the $name plugin" )
499 if $ENV{'TEST_VERBOSE'};
501 my $etc_path = $plugin->Path('etc');
502 Test::More::diag( "etc path of the plugin is '$etc_path'" )
503 if $ENV{'TEST_VERBOSE'};
505 unless ( -e $etc_path ) {
506 # We can't tell if the plugin has no data, or we screwed up the etc/ path
507 Test::More::ok(1, "There is no etc dir: no schema" );
508 Test::More::ok(1, "There is no etc dir: no ACLs" );
509 Test::More::ok(1, "There is no etc dir: no data" );
513 __reconnect_rt('as dba');
516 my ($ret, $msg) = $RT::Handle->InsertSchema( undef, $etc_path );
517 Test::More::ok($ret || $msg =~ /^Couldn't find schema/, "Created schema: ".($msg||''));
521 my ($ret, $msg) = $RT::Handle->InsertACL( undef, $etc_path );
522 Test::More::ok($ret || $msg =~ /^Couldn't find ACLs/, "Created ACL: ".($msg||''));
526 my $data_file = File::Spec->catfile( $etc_path, 'initialdata' );
527 if ( -e $data_file ) {
529 my ($ret, $msg) = $RT::Handle->InsertData( $data_file );;
530 Test::More::ok($ret, "Inserted data".($msg||''));
532 Test::More::ok(1, "There is no data file" );
539 my ($dsn, $user, $pass) = @_;
540 if ( $dsn =~ /Oracle/i ) {
541 $ENV{'NLS_LANG'} = "AMERICAN_AMERICA.AL32UTF8";
542 $ENV{'NLS_NCHAR'} = "AL32UTF8";
544 my $dbh = DBI->connect(
546 { RaiseError => 0, PrintError => 1 },
549 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
550 print STDERR $msg; exit -1;
555 sub __create_database {
556 # bootstrap with dba cred
558 RT::Handle->SystemDSN,
559 $ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
562 unless ( $ENV{RT_TEST_PARALLEL} ) {
563 # already dropped db in parallel tests, need to do so for other cases.
564 __drop_database( $dbh );
567 RT::Handle->CreateDatabase( $dbh );
572 sub __drop_database {
575 # Pg doesn't like if you issue a DROP DATABASE while still connected
576 # it's still may fail if web-server is out there and holding a connection
579 my $my_dbh = $dbh? 0 : 1;
581 RT::Handle->SystemDSN,
582 $ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
585 # We ignore errors intentionally by not checking the return value of
586 # DropDatabase below, so let's also suppress DBI's printing of errors when
587 # we overzealously drop.
588 local $dbh->{PrintError} = 0;
589 local $dbh->{PrintWarn} = 0;
591 RT::Handle->DropDatabase( $dbh );
592 $dbh->disconnect if $my_dbh;
599 # look at %DBIHandle and $PrevHandle in DBIx::SB::Handle for explanation
600 $RT::Handle = RT::Handle->new;
601 $RT::Handle->dbh( undef );
602 $RT::Handle->Connect(
604 ? (User => $ENV{RT_DBA_USER}, Password => $ENV{RT_DBA_PASSWORD})
607 $RT::Handle->PrintError;
608 $RT::Handle->dbh->{PrintError} = 1;
609 return $RT::Handle->dbh;
612 sub __disconnect_rt {
613 # look at %DBIHandle and $PrevHandle in DBIx::SB::Handle for explanation
614 $RT::Handle->dbh->disconnect if $RT::Handle and $RT::Handle->dbh;
616 %DBIx::SearchBuilder::Handle::DBIHandle = ();
617 $DBIx::SearchBuilder::Handle::PrevHandle = undef;
621 delete $RT::System->{attributes};
623 DBIx::SearchBuilder::Record::Cachable->FlushCache
624 if DBIx::SearchBuilder::Record::Cachable->can("FlushCache");
630 # We use local to ensure that the $filter we grab is from InitLogging
631 # and not the handler generated by a previous call to this function
633 local $SIG{__WARN__};
635 $filter = $SIG{__WARN__};
637 $SIG{__WARN__} = sub {
639 my $status = $filter->(@_);
640 if ($status and $status eq 'IGNORE') {
641 return; # pretend the bad dream never happened
644 # Avoid reporting this anonymous call frame as the source of the warning.
645 goto &$Test_NoWarnings_Catcher;
652 =head2 load_or_create_user
656 sub load_or_create_user {
658 my %args = ( Privileged => 1, Disabled => 0, @_ );
660 my $MemberOf = delete $args{'MemberOf'};
661 $MemberOf = [ $MemberOf ] if defined $MemberOf && !ref $MemberOf;
664 my $obj = RT::User->new( RT->SystemUser );
665 if ( $args{'Name'} ) {
666 $obj->LoadByCols( Name => $args{'Name'} );
667 } elsif ( $args{'EmailAddress'} ) {
668 $obj->LoadByCols( EmailAddress => $args{'EmailAddress'} );
670 die "Name or EmailAddress is required";
674 $obj->SetPrivileged( $args{'Privileged'} || 0 )
675 if ($args{'Privileged'}||0) != ($obj->Privileged||0);
676 $obj->SetDisabled( $args{'Disabled'} || 0 )
677 if ($args{'Disabled'}||0) != ($obj->Disabled||0);
679 my ($val, $msg) = $obj->Create( %args );
680 die "$msg" unless $val;
683 # clean group membership
685 require RT::GroupMembers;
686 my $gms = RT::GroupMembers->new( RT->SystemUser );
687 my $groups_alias = $gms->Join(
688 FIELD1 => 'GroupId', TABLE2 => 'Groups', FIELD2 => 'id',
690 $gms->Limit( ALIAS => $groups_alias, FIELD => 'Domain', VALUE => 'UserDefined' );
691 $gms->Limit( FIELD => 'MemberId', VALUE => $obj->id );
692 while ( my $group_member_record = $gms->Next ) {
693 $group_member_record->Delete;
697 # add new user to groups
698 foreach ( @$MemberOf ) {
699 my $group = RT::Group->new( RT::SystemUser() );
700 $group->LoadUserDefinedGroup( $_ );
701 die "couldn't load group '$_'" unless $group->id;
702 $group->AddMember( $obj->id );
708 =head2 load_or_create_queue
712 sub load_or_create_queue {
714 my %args = ( Disabled => 0, @_ );
715 my $obj = RT::Queue->new( RT->SystemUser );
716 if ( $args{'Name'} ) {
717 $obj->LoadByCols( Name => $args{'Name'} );
719 die "Name is required";
721 unless ( $obj->id ) {
722 my ($val, $msg) = $obj->Create( %args );
723 die "$msg" unless $val;
725 my @fields = qw(CorrespondAddress CommentAddress);
726 foreach my $field ( @fields ) {
727 next unless exists $args{ $field };
728 next if $args{ $field } eq ($obj->$field || '');
730 no warnings 'uninitialized';
731 my $method = 'Set'. $field;
732 my ($val, $msg) = $obj->$method( $args{ $field } );
733 die "$msg" unless $val;
740 sub delete_queue_watchers {
744 foreach my $q ( @queues ) {
745 foreach my $t (qw(Cc AdminCc) ) {
746 $q->DeleteWatcher( Type => $t, PrincipalId => $_->MemberId )
747 foreach @{ $q->$t()->MembersObj->ItemsArrayRef };
753 local $Test::Builder::Level = $Test::Builder::Level + 1;
756 my $defaults = shift;
758 @data = sort { rand(100) <=> rand(100) } @data
759 if delete $defaults->{'RandomOrder'};
761 $defaults->{'Queue'} ||= 'General';
765 my %args = %{ shift @data };
766 $args{$_} = $res[ $args{$_} ]->id foreach
767 grep $args{ $_ }, keys %RT::Ticket::LINKTYPEMAP;
768 push @res, $self->create_ticket( %$defaults, %args );
774 local $Test::Builder::Level = $Test::Builder::Level + 1;
779 if ($args{Queue} && $args{Queue} =~ /\D/) {
780 my $queue = RT::Queue->new(RT->SystemUser);
781 if (my $id = $queue->Load($args{Queue}) ) {
784 die ("Error: Invalid queue $args{Queue}");
788 if ( my $content = delete $args{'Content'} ) {
789 $args{'MIMEObj'} = MIME::Entity->build(
790 From => $args{'Requestor'},
791 Subject => $args{'Subject'},
796 my $ticket = RT::Ticket->new( RT->SystemUser );
797 my ( $id, undef, $msg ) = $ticket->Create( %args );
798 Test::More::ok( $id, "ticket created" )
799 or Test::More::diag("error: $msg");
801 # hackish, but simpler
802 if ( $args{'LastUpdatedBy'} ) {
803 $ticket->__Set( Field => 'LastUpdatedBy', Value => $args{'LastUpdatedBy'} );
807 for my $field ( keys %args ) {
808 #TODO check links and watchers
810 if ( $field =~ /CustomField-(\d+)/ ) {
812 my $got = join ',', sort map $_->Content,
813 @{ $ticket->CustomFieldValues($cf)->ItemsArrayRef };
814 my $expected = ref $args{$field}
815 ? join( ',', sort @{ $args{$field} } )
817 Test::More::is( $got, $expected, 'correct CF values' );
820 next if ref $args{$field};
821 next unless $ticket->can($field) or $ticket->_Accessible($field,"read");
822 next if ref $ticket->$field();
823 Test::More::is( $ticket->$field(), $args{$field}, "$field is correct" );
833 my $tickets = RT::Tickets->new( RT->SystemUser );
835 $tickets->FromSQL( $query );
840 while ( my $ticket = $tickets->Next ) {
845 =head2 load_or_create_custom_field
849 sub load_or_create_custom_field {
851 my %args = ( Disabled => 0, @_ );
852 my $obj = RT::CustomField->new( RT->SystemUser );
853 if ( $args{'Name'} ) {
854 $obj->LoadByName( Name => $args{'Name'}, Queue => $args{'Queue'} );
856 die "Name is required";
858 unless ( $obj->id ) {
859 my ($val, $msg) = $obj->Create( %args );
860 die "$msg" unless $val;
869 $current = $current ? RT::CurrentUser->new($current) : RT->SystemUser;
870 my $tickets = RT::Tickets->new( $current );
871 $tickets->OrderBy( FIELD => 'id', ORDER => 'DESC' );
872 $tickets->Limit( FIELD => 'id', OPERATOR => '>', VALUE => '0' );
873 $tickets->RowsPerPage( 1 );
874 return $tickets->First;
882 RT::ACE->new( RT->SystemUser );
883 my @fields = keys %{ RT::ACE->_ClassAccessible };
886 my $acl = RT::ACL->new( RT->SystemUser );
887 $acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
890 while ( my $ace = $acl->Next ) {
891 my $obj = $ace->PrincipalObj->Object;
892 if ( $obj->isa('RT::Group') && $obj->Type eq 'UserEquiv' && $obj->Instance == RT->Nobody->id ) {
897 foreach my $field( @fields ) {
898 $tmp{ $field } = $ace->__Value( $field );
908 foreach my $entry ( @entries ) {
909 my $ace = RT::ACE->new( RT->SystemUser );
910 my ($status, $msg) = $ace->RT::Record::Create( %$entry );
912 Test::More::diag "couldn't create a record: $msg";
921 my $acl = RT::ACL->new( RT->SystemUser );
922 $acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
923 while ( my $ace = $acl->Next ) {
924 my $obj = $ace->PrincipalObj->Object;
925 if ( $obj->isa('RT::Group') && $obj->Type eq 'UserEquiv' && $obj->Instance == RT->Nobody->id ) {
930 return $self->add_rights( @_ );
935 my @list = ref $_[0]? @_: @_? { @_ }: ();
938 foreach my $e (@list) {
939 my $principal = delete $e->{'Principal'};
940 unless ( ref $principal ) {
941 if ( $principal =~ /^(everyone|(?:un)?privileged)$/i ) {
942 $principal = RT::Group->new( RT->SystemUser );
943 $principal->LoadSystemInternalGroup($1);
944 } elsif ( $principal =~ /^(Owner|Requestor|(?:Admin)?Cc)$/i ) {
945 $principal = RT::Group->new( RT->SystemUser );
946 $principal->LoadByCols(
947 Domain => (ref($e->{'Object'})||'RT::System').'-Role',
949 ref($e->{'Object'})? (Instance => $e->{'Object'}->id): (),
952 die "principal is not an object, but also is not name of a system group";
955 unless ( $principal->isa('RT::Principal') ) {
956 if ( $principal->can('PrincipalObj') ) {
957 $principal = $principal->PrincipalObj;
960 my @rights = ref $e->{'Right'}? @{ $e->{'Right'} }: ($e->{'Right'});
961 foreach my $right ( @rights ) {
962 my ($status, $msg) = $principal->GrantRight( %$e, Right => $right );
963 $RT::Logger->debug($msg);
972 require RT::Test::Web;
974 url => RT::Test::Web->rt_base_url,
976 action => 'correspond',
979 command => $RT::BinPath .'/rt-mailgate',
982 my $message = delete $args{'message'};
984 $args{after_open} = sub {
985 my $child_in = shift;
986 if ( UNIVERSAL::isa($message, 'MIME::Entity') ) {
987 $message->print( $child_in );
989 print $child_in $message;
993 $self->run_and_capture(%args);
996 sub run_and_capture {
1000 my $after_open = delete $args{after_open};
1002 my $cmd = delete $args{'command'};
1003 die "Couldn't find command ($cmd)" unless -f $cmd;
1005 $cmd .= ' --debug' if delete $args{'debug'};
1007 while( my ($k,$v) = each %args ) {
1009 $cmd .= " --$k '$v'";
1013 DBIx::SearchBuilder::Record::Cachable->FlushCache;
1016 my ($child_out, $child_in);
1017 my $pid = IPC::Open2::open2($child_out, $child_in, $cmd);
1019 $after_open->($child_in, $child_out) if $after_open;
1023 my $result = do { local $/; <$child_out> };
1026 return ($?, $result);
1029 sub send_via_mailgate_and_http {
1031 my $message = shift;
1034 my ($status, $gate_result) = $self->run_mailgate(
1035 message => $message, %args
1039 unless ( $status >> 8 ) {
1040 ($id) = ($gate_result =~ /Ticket:\s*(\d+)/i);
1042 Test::More::diag "Couldn't find ticket id in text:\n$gate_result"
1043 if $ENV{'TEST_VERBOSE'};
1046 Test::More::diag "Mailgate output:\n$gate_result"
1047 if $ENV{'TEST_VERBOSE'};
1049 return ($status, $id);
1053 sub send_via_mailgate {
1055 my $message = shift;
1056 my %args = ( action => 'correspond',
1061 if ( UNIVERSAL::isa( $message, 'MIME::Entity' ) ) {
1062 $message = $message->as_string;
1065 my ( $status, $error_message, $ticket )
1066 = RT::Interface::Email::Gateway( {%args, message => $message} );
1067 return ( $status, $ticket ? $ticket->id : 0 );
1072 sub open_mailgate_ok {
1074 my $baseurl = shift;
1075 my $queue = shift || 'general';
1076 my $action = shift || 'correspond';
1077 Test::More::ok(open(my $mail, '|-', "$RT::BinPath/rt-mailgate --url $baseurl --queue $queue --action $action"), "Opened the mailgate - $!");
1082 sub close_mailgate_ok {
1086 Test::More::is ($? >> 8, 0, "The mail gateway exited normally. yay");
1091 my $expected = shift;
1093 my $mailsent = scalar grep /\S/, split /%% split me! %%\n/,
1094 RT::Test->file_content(
1101 $mailsent, $expected,
1102 "The number of mail sent ($expected) matches. yay"
1106 sub fetch_caught_mails {
1108 return grep /\S/, split /%% split me! %%\n/,
1109 RT::Test->file_content(
1116 sub clean_caught_mails {
1117 unlink $tmp{'mailbox'};
1120 =head2 get_relocatable_dir
1122 Takes a path relative to the location of the test file that is being
1123 run and returns a path that takes the invocation path into account.
1125 e.g. C<RT::Test::get_relocatable_dir(File::Spec->updir(), 'data', 'emails')>
1127 Parent directory traversals (C<..> or File::Spec->updir()) are naively
1128 canonicalized based on the test file path (C<$0>) so that symlinks aren't
1129 followed. This is the exact opposite behaviour of most filesystems and is
1130 considered "wrong", however it is necessary for some subsets of tests which are
1131 symlinked into the testing tree.
1135 sub get_relocatable_dir {
1136 my @directories = File::Spec->splitdir(
1137 File::Spec->rel2abs((File::Spec->splitpath($0))[1])
1139 push @directories, File::Spec->splitdir($_) for @_;
1142 for (@directories) {
1143 if ($_ eq "..") { pop @clean }
1144 elsif ($_ ne ".") { push @clean, $_ }
1146 return File::Spec->catdir(@clean);
1149 =head2 get_relocatable_file
1151 Same as get_relocatable_dir, but takes a file and a path instead
1154 e.g. RT::Test::get_relocatable_file('test-email',
1155 (File::Spec->updir(), 'data', 'emails'))
1159 sub get_relocatable_file {
1161 return File::Spec->catfile(get_relocatable_dir(@_), $file);
1164 sub get_abs_relocatable_dir {
1165 (my $volume, my $directories, my $file) = File::Spec->splitpath($0);
1166 if (File::Spec->file_name_is_absolute($directories)) {
1167 return File::Spec->catdir($directories, @_);
1169 return File::Spec->catdir(Cwd->getcwd(), $directories, @_);
1176 DIR => $tmp{directory},
1181 sub import_gnupg_key {
1184 my $type = shift || 'secret';
1186 $key =~ s/\@/-at-/g;
1187 $key .= ".$type.key";
1189 require RT::Crypt::GnuPG;
1191 # simple strategy find data/gnupg/keys, from the dir where test file lives
1192 # to updirs, try 3 times in total
1193 my $path = File::Spec->catfile( 'data', 'gnupg', 'keys' );
1195 for my $up ( 0 .. 2 ) {
1196 my $p = get_relocatable_dir($path);
1202 $path = File::Spec->catfile( File::Spec->updir(), $path );
1206 die "can't find the dir where gnupg keys are stored"
1209 return RT::Crypt::GnuPG::ImportKey(
1210 RT::Test->file_content( [ $abs_path, $key ] ) );
1214 sub lsign_gnupg_key {
1218 require RT::Crypt::GnuPG; require GnuPG::Interface;
1219 my $gnupg = GnuPG::Interface->new();
1220 my %opt = RT->Config->Get('GnuPGOptions');
1221 $gnupg->options->hash_init(
1222 RT::Crypt::GnuPG::_PrepareGnuPGOptions( %opt ),
1223 meta_interactive => 0,
1227 my $handles = GnuPG::Handles->new(
1228 stdin => ($handle{'input'} = IO::Handle->new()),
1229 stdout => ($handle{'output'} = IO::Handle->new()),
1230 stderr => ($handle{'error'} = IO::Handle->new()),
1231 logger => ($handle{'logger'} = IO::Handle->new()),
1232 status => ($handle{'status'} = IO::Handle->new()),
1233 command => ($handle{'command'} = IO::Handle->new()),
1237 local $SIG{'CHLD'} = 'DEFAULT';
1238 local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
1239 my $pid = $gnupg->wrap_call(
1240 handles => $handles,
1241 commands => ['--lsign-key'],
1242 command_args => [$key],
1244 close $handle{'input'};
1245 while ( my $str = readline $handle{'status'} ) {
1246 if ( $str =~ /^\[GNUPG:\]\s*GET_BOOL sign_uid\..*/ ) {
1247 print { $handle{'command'} } "y\n";
1253 close $handle{'output'};
1256 $res{'exit_code'} = $?;
1257 foreach ( qw(error logger status) ) {
1258 $res{$_} = do { local $/; readline $handle{$_} };
1259 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1262 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1263 $RT::Logger->warning( $res{'error'} ) if $res{'error'};
1264 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1265 if ( $err || $res{'exit_code'} ) {
1266 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
1271 sub trust_gnupg_key {
1275 require RT::Crypt::GnuPG; require GnuPG::Interface;
1276 my $gnupg = GnuPG::Interface->new();
1277 my %opt = RT->Config->Get('GnuPGOptions');
1278 $gnupg->options->hash_init(
1279 RT::Crypt::GnuPG::_PrepareGnuPGOptions( %opt ),
1280 meta_interactive => 0,
1284 my $handles = GnuPG::Handles->new(
1285 stdin => ($handle{'input'} = IO::Handle->new()),
1286 stdout => ($handle{'output'} = IO::Handle->new()),
1287 stderr => ($handle{'error'} = IO::Handle->new()),
1288 logger => ($handle{'logger'} = IO::Handle->new()),
1289 status => ($handle{'status'} = IO::Handle->new()),
1290 command => ($handle{'command'} = IO::Handle->new()),
1294 local $SIG{'CHLD'} = 'DEFAULT';
1295 local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
1296 my $pid = $gnupg->wrap_call(
1297 handles => $handles,
1298 commands => ['--edit-key'],
1299 command_args => [$key],
1301 close $handle{'input'};
1304 while ( my $str = readline $handle{'status'} ) {
1305 if ( $str =~ /^\[GNUPG:\]\s*\QGET_LINE keyedit.prompt/ ) {
1307 print { $handle{'command'} } "quit\n";
1309 print { $handle{'command'} } "trust\n";
1311 } elsif ( $str =~ /^\[GNUPG:\]\s*\QGET_LINE edit_ownertrust.value/ ) {
1312 print { $handle{'command'} } "5\n";
1313 } elsif ( $str =~ /^\[GNUPG:\]\s*\QGET_BOOL edit_ownertrust.set_ultimate.okay/ ) {
1314 print { $handle{'command'} } "y\n";
1321 close $handle{'output'};
1324 $res{'exit_code'} = $?;
1325 foreach ( qw(error logger status) ) {
1326 $res{$_} = do { local $/; readline $handle{$_} };
1327 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1330 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1331 $RT::Logger->warning( $res{'error'} ) if $res{'error'};
1332 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1333 if ( $err || $res{'exit_code'} ) {
1334 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
1342 require RT::Test::Web;
1344 if ($rttest_opt{nodb} and not $rttest_opt{server_ok}) {
1345 die "You are trying to use a test web server without a database. "
1346 ."You may want noinitialdata => 1 instead. "
1347 ."Pass server_ok => 1 if you know what you're doing.";
1351 $ENV{'RT_TEST_WEB_HANDLER'} = undef
1352 if $rttest_opt{actual_server} && ($ENV{'RT_TEST_WEB_HANDLER'}||'') eq 'inline';
1353 $ENV{'RT_TEST_WEB_HANDLER'} ||= 'plack';
1354 my $which = $ENV{'RT_TEST_WEB_HANDLER'};
1355 my ($server, $variant) = split /\+/, $which, 2;
1357 my $function = 'start_'. $server .'_server';
1358 unless ( $self->can($function) ) {
1359 die "Don't know how to start server '$server'";
1361 return $self->$function( variant => $variant, @_ );
1366 my %server_opt = @_;
1371 open( my $warn_fh, ">", \$warnings );
1372 local *STDERR = $warn_fh;
1374 if ($server_opt{variant} and $server_opt{variant} eq 'rt-server') {
1376 my $file = "$RT::SbinPath/rt-server";
1377 my $psgi = do $file;
1379 die "Couldn't parse $file: $@" if $@;
1380 die "Couldn't do $file: $!" unless defined $psgi;
1381 die "Couldn't run $file" unless $psgi;
1386 require RT::Interface::Web::Handler;
1387 $app = RT::Interface::Web::Handler->PSGIApp;
1390 require Plack::Middleware::Test::StashWarnings;
1391 my $stashwarnings = Plack::Middleware::Test::StashWarnings->new;
1392 $app = $stashwarnings->wrap($app);
1394 if ($server_opt{basic_auth}) {
1395 require Plack::Middleware::Auth::Basic;
1396 $app = Plack::Middleware::Auth::Basic->wrap(
1398 authenticator => sub {
1399 my ($username, $password) = @_;
1400 return $username eq 'root' && $password eq 'password';
1406 $stashwarnings->add_warning( $warnings ) if $warnings;
1411 sub start_plack_server {
1414 require Plack::Loader;
1415 my $plack_server = Plack::Loader->load
1418 server_ready => sub {
1419 kill 'USR1' => getppid();
1422 # We are expecting a USR1 from the child process after it's ready
1423 # to listen. We set this up _before_ we fork to avoid race
1426 local $SIG{USR1} = sub { $handled = 1};
1430 die "failed to fork" unless defined $pid;
1433 sleep 15 unless $handled;
1434 Test::More::diag "did not get expected USR1 for test server readiness"
1436 push @SERVERS, $pid;
1437 my $Tester = Test::Builder->new;
1438 $Tester->ok(1, "started plack server ok");
1441 unless $rttest_opt{nodb};
1442 return ("http://localhost:$port", RT::Test::Web->new);
1446 if ( $^O !~ /MSWin32/ ) {
1448 or die "Can't start a new session: $!";
1451 # stick this in a scope so that when $app is garbage collected,
1452 # StashWarnings can complain about unhandled warnings
1454 $plack_server->run($self->test_app(@_));
1461 sub start_inline_server {
1464 require Test::WWW::Mechanize::PSGI;
1465 unshift @RT::Test::Web::ISA, 'Test::WWW::Mechanize::PSGI';
1467 # Clear out squished CSS and JS cache, since it's retained across
1468 # servers, since it's in-process
1469 RT::Interface::Web->ClearSquished;
1470 require RT::Interface::Web::Request;
1471 RT::Interface::Web::Request->clear_callback_cache;
1473 Test::More::ok(1, "psgi test server ok");
1474 $TEST_APP = $self->test_app(@_);
1475 return ("http://localhost:$port", RT::Test::Web->new);
1478 sub start_apache_server {
1480 my %server_opt = @_;
1481 $server_opt{variant} ||= 'mod_perl';
1482 $ENV{RT_TEST_WEB_HANDLER} = "apache+$server_opt{variant}";
1484 require RT::Test::Apache;
1485 my $pid = RT::Test::Apache->start_server(
1490 push @SERVERS, $pid;
1492 my $url = RT->Config->Get('WebURL');
1494 return ($url, RT::Test::Web->new);
1500 return unless @SERVERS;
1503 $sig = 'INT' if $ENV{'RT_TEST_WEB_HANDLER'} eq "plack";
1504 kill $sig, @SERVERS;
1505 foreach my $pid (@SERVERS) {
1506 if ($ENV{RT_TEST_WEB_HANDLER} =~ /^apache/) {
1507 sleep 1 while kill 0, $pid;
1516 sub temp_directory {
1517 return $tmp{'directory'};
1525 $path = File::Spec->catfile( @$path ) if ref $path eq 'ARRAY';
1527 Test::More::diag "reading content of '$path'" if $ENV{'TEST_VERBOSE'};
1529 open( my $fh, "<:raw", $path )
1531 warn "couldn't open file '$path': $!" unless $args{noexist};
1534 my $content = do { local $/; <$fh> };
1537 unlink $path if $args{'unlink'};
1542 sub find_executable {
1547 foreach my $dir ( split /:/, $ENV{'PATH'} ) {
1548 my $fpath = File::Spec->catpath(
1549 (File::Spec->splitpath( $dir, 'no file' ))[0..1], $name
1551 next unless -e $fpath && -r _ && -x _;
1558 return unless $ENV{RT_TEST_VERBOSE} || $ENV{TEST_VERBOSE};
1559 goto \&Test::More::diag;
1564 require RT::EmailParser;
1565 my $parser = RT::EmailParser->new;
1566 $parser->ParseMIMEEntityFromScalar( $mail );
1567 return $parser->Entity;
1571 Test::More::ok($_[0], $_[1] || 'This works');
1575 Test::More::ok(!$_[0], $_[1] || 'This should fail');
1579 my ($cmd, @args) = @_;
1580 my $builder = RT::Test->builder;
1582 if ($cmd eq "skip_all") {
1583 $check_warnings_in_end = 0;
1584 } elsif ($cmd eq "tests") {
1585 # Increment the test count for the warnings check
1588 $builder->plan($cmd, @args);
1592 my $builder = RT::Test->builder;
1594 Test::NoWarnings::had_no_warnings();
1595 $check_warnings_in_end = 0;
1597 $builder->done_testing(@_);
1601 my $Test = RT::Test->builder;
1602 return if $Test->{Original_Pid} != $$;
1604 # we are in END block and should protect our exit code
1605 # so calls below may call system or kill that clobbers $?
1608 Test::NoWarnings::had_no_warnings() if $check_warnings_in_end;
1610 RT::Test->stop_server(1);
1613 if ( !$Test->is_passing ) {
1614 $tmp{'directory'}->unlink_on_destroy(0);
1617 "Some tests failed or we bailed out, tmp directory"
1618 ." '$tmp{directory}' is not cleaned"
1622 if ( $ENV{RT_TEST_PARALLEL} && $created_new_db ) {
1626 # Drop our port from t/tmp/ports; do this after dropping the
1627 # database, as our port lock is also a lock on the database name.
1630 my $portfile = "$tmp{'directory'}/../ports";
1631 sysopen(PORTS, $portfile, O_RDWR|O_CREAT)
1632 or die "Can't write to ports file $portfile: $!";
1633 flock(PORTS, LOCK_EX)
1634 or die "Can't write-lock ports file $portfile: $!";
1635 $ports{$_}++ for split ' ', join("",<PORTS>);
1636 delete $ports{$port};
1639 print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
1640 close(PORTS) or die "Can't close ports file: $!";
1645 # ease the used only once warning
1648 %{'RT::I18N::en_us::Lexicon'};
1649 %{'Win32::Locale::Lexicon'};