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 # Spit out a plan (if we got one) *before* we load modules
115 if ( $args{'tests'} ) {
116 plan( tests => $args{'tests'} )
117 unless $args{'tests'} eq 'no_declare';
119 elsif ( exists $args{'tests'} ) {
120 # do nothing if they say "tests => undef" - let them make the plan
122 elsif ( $args{'skip_all'} ) {
123 plan(skip_all => $args{'skip_all'});
126 $class->builder->no_plan unless $class->builder->has_plan;
129 push @{ $args{'plugins'} ||= [] }, @{ $args{'requires'} }
130 if $args{'requires'};
131 push @{ $args{'plugins'} ||= [] }, $args{'testing'}
134 $class->bootstrap_tempdir;
136 $class->bootstrap_port;
138 $class->bootstrap_plugins_paths( %args );
140 $class->bootstrap_config( %args );
145 if (RT->Config->Get('DevelMode')) { require Module::Refresh; }
147 RT::InitPluginPaths();
150 $class->bootstrap_db( %args );
160 RT->Config->PostLoadCheck;
162 $class->set_config_wrapper;
164 my $screen_logger = $RT::Logger->remove( 'screen' );
165 require Log::Dispatch::Perl;
166 $RT::Logger->add( Log::Dispatch::Perl->new
168 min_level => $screen_logger->min_level,
169 action => { error => 'warn',
170 critical => 'warn' } ) );
172 # XXX: this should really be totally isolated environment so we
173 # can parallelize and be sane
174 mkpath [ $RT::MasonSessionDir ]
175 if RT->Config->Get('DatabaseType');
178 while ( my ($package) = caller($level-1) ) {
179 last unless $package =~ /Test/;
183 Test::More->export_to_level($level);
184 Test::NoWarnings->export_to_level($level);
186 # Blow away symbols we redefine to avoid warnings.
187 # better than "no warnings 'redefine'" because we might accidentally
188 # suppress a mistaken redefinition
190 delete ${ caller($level) . '::' }{diag};
191 delete ${ caller($level) . '::' }{plan};
192 delete ${ caller($level) . '::' }{done_testing};
193 __PACKAGE__->export_to_level($level);
198 local $Test::Builder::Level = $Test::Builder::Level + 1;
199 return Test::More::ok(1, $d) unless defined $v;
200 return Test::More::ok(1, $d) unless length $v;
201 return Test::More::is($v, '', $d);
204 my $created_new_db; # have we created new db? mainly for parallel testing
206 sub db_requires_no_dba {
208 my $db_type = RT->Config->Get('DatabaseType');
209 return 1 if $db_type eq 'SQLite';
217 # Determine which ports are in use
218 use Fcntl qw(:DEFAULT :flock);
219 my $portfile = "$tmp{'directory'}/../ports";
220 sysopen(PORTS, $portfile, O_RDWR|O_CREAT)
221 or die "Can't write to ports file $portfile: $!";
222 flock(PORTS, LOCK_EX)
223 or die "Can't write-lock ports file $portfile: $!";
224 $ports{$_}++ for split ' ', join("",<PORTS>);
226 # Pick a random port, checking that the port isn't in our in-use
227 # list, and that something isn't already listening there.
229 $port = 1024 + int rand(10_000) + $$ % 1024;
230 redo if $ports{$port};
232 # There is a race condition in here, where some non-RT::Test
233 # process claims the port after we check here but before our
234 # server binds. However, since we mostly care about race
235 # conditions with ourselves under high concurrency, this is
236 # generally good enough.
237 my $paddr = sockaddr_in( $port, inet_aton('localhost') );
238 socket( SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp') )
240 if ( connect( SOCK, $paddr ) ) {
249 # Write back out the in-use ports
252 print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
253 close(PORTS) or die "Can't close ports file: $!";
256 sub bootstrap_tempdir {
258 my ($test_dir, $test_file) = ('t', '');
260 if (File::Spec->rel2abs($0) =~ m{(?:^|[\\/])(x?t)[/\\](.*)}) {
263 $test_file =~ s{[/\\]}{-}g;
266 my $dir_name = File::Spec->rel2abs("$test_dir/tmp");
268 return $tmp{'directory'} = File::Temp->newdir(
269 "${test_file}XXXXXXXX",
274 sub bootstrap_config {
278 $tmp{'config'}{'RT'} = File::Spec->catfile(
279 "$tmp{'directory'}", 'RT_SiteConfig.pm'
281 open( my $config, '>', $tmp{'config'}{'RT'} )
282 or die "Couldn't open $tmp{'config'}{'RT'}: $!";
284 my $dbname = $ENV{RT_TEST_PARALLEL}? "rt4test_$port" : "rt4test";
286 Set( \$WebDomain, "localhost");
287 Set( \$WebPort, $port);
289 Set( \@LexiconLanguages, qw(en zh_TW fr ja));
290 Set( \$RTAddressRegexp , qr/^bad_re_that_doesnt_match\$/i);
292 if ( $ENV{'RT_TEST_DB_SID'} ) { # oracle case
293 print $config "Set( \$DatabaseName , '$ENV{'RT_TEST_DB_SID'}' );\n";
294 print $config "Set( \$DatabaseUser , '$dbname');\n";
296 print $config "Set( \$DatabaseName , '$dbname');\n";
297 print $config "Set( \$DatabaseUser , 'u${dbname}');\n";
300 if ( $args{'plugins'} ) {
301 print $config "Set( \@Plugins, qw(". join( ' ', @{ $args{'plugins'} } ) .") );\n";
303 my $plugin_data = File::Spec->rel2abs("t/data/plugins");
304 print $config qq[\$RT::PluginPath = "$plugin_data";\n];
307 if ( $INC{'Devel/Cover.pm'} ) {
308 print $config "Set( \$DevelMode, 0 );\n";
310 elsif ( $ENV{RT_TEST_DEVEL} ) {
311 print $config "Set( \$DevelMode, 1 );\n";
314 print $config "Set( \$DevelMode, 0 );\n";
317 $self->bootstrap_logging( $config );
320 my $mail_catcher = $tmp{'mailbox'} = File::Spec->catfile(
321 $tmp{'directory'}->dirname, 'mailbox.eml'
324 Set( \$MailCommand, sub {
327 open( my \$handle, '>>', '$mail_catcher' )
328 or die "Unable to open '$mail_catcher' for appending: \$!";
330 \$MIME->print(\$handle);
331 print \$handle "%% split me! %%\n";
336 $self->bootstrap_more_config($config, \%args);
338 print $config $args{'config'} if $args{'config'};
340 print $config "\n1;\n";
341 $ENV{'RT_SITE_CONFIG'} = $tmp{'config'}{'RT'};
347 sub bootstrap_more_config { }
349 sub bootstrap_logging {
353 # prepare file for logging
354 $tmp{'log'}{'RT'} = File::Spec->catfile(
355 "$tmp{'directory'}", 'rt.debug.log'
357 open( my $fh, '>', $tmp{'log'}{'RT'} )
358 or die "Couldn't open $tmp{'config'}{'RT'}: $!";
359 # make world writable so apache under different user
361 chmod 0666, $tmp{'log'}{'RT'};
364 Set( \$LogToSyslog , undef);
365 Set( \$LogToScreen , "warning");
366 Set( \$LogToFile, 'debug' );
367 Set( \$LogDir, q{$tmp{'directory'}} );
368 Set( \$LogToFileNamed, 'rt.debug.log' );
372 sub set_config_wrapper {
375 my $old_sub = \&RT::Config::Set;
376 no warnings 'redefine';
377 *RT::Config::Set = sub {
378 # Determine if the caller is either from a test script, or
379 # from helper functions called by test script to alter
380 # configuration that should be written. This is necessary
381 # because some extensions (RTIR, for example) temporarily swap
382 # configuration values out and back in Mason during requests.
383 my @caller = caller(1); # preserve list context
384 @caller = caller(0) unless @caller;
386 if ( ($caller[1]||'') =~ /\.t$/) {
387 my ($self, $name) = @_;
388 my $type = $RT::Config::META{$name}->{'Type'} || 'SCALAR';
394 my $sigil = $sigils{$type} || $sigils{'SCALAR'};
395 open( my $fh, '>>', $tmp{'config'}{'RT'} )
396 or die "Couldn't open config file: $!";
397 require Data::Dumper;
398 local $Data::Dumper::Terse = 1;
399 my $dump = Data::Dumper::Dumper([@_[2 .. $#_]]);
402 "\nSet(${sigil}${name}, \@{". $dump ."});\n1;\n";
406 warn "you're changing config option in a test file"
407 ." when server is active";
410 return $old_sub->(@_);
418 unless (defined $ENV{'RT_DBA_USER'} && defined $ENV{'RT_DBA_PASSWORD'}) {
419 Test::More::BAIL_OUT(
420 "RT_DBA_USER and RT_DBA_PASSWORD environment variables need"
421 ." to be set in order to run 'make test'"
422 ) unless $self->db_requires_no_dba;
426 if (my $forceopt = $ENV{RT_TEST_FORCE_OPT}) {
427 Test::More::diag "forcing $forceopt";
431 # Short-circuit the rest of ourselves if we don't want a db
437 my $db_type = RT->Config->Get('DatabaseType');
439 __reconnect_rt('as dba');
440 $RT::Handle->InsertSchema;
441 $RT::Handle->InsertACL unless $db_type eq 'Oracle';
446 $RT::Handle->InsertInitialData
447 unless $args{noinitialdata};
449 $RT::Handle->InsertData( $RT::EtcPath . "/initialdata" )
450 unless $args{noinitialdata} or $args{nodata};
452 $self->bootstrap_plugins_db( %args );
455 sub bootstrap_plugins_paths {
459 return unless $args{'plugins'};
460 my @plugins = @{ $args{'plugins'} };
463 if ( $args{'testing'} ) {
465 $cwd = Cwd::getcwd();
469 my $old_func = \&RT::Plugin::_BasePath;
470 no warnings 'redefine';
471 *RT::Plugin::_BasePath = sub {
472 my $name = $_[0]->{'name'};
474 return $cwd if $args{'testing'} && $name eq $args{'testing'};
476 if ( grep $name eq $_, @plugins ) {
477 my $variants = join "(?:|::|-|_)", map "\Q$_\E", split /::/, $name;
478 my ($path) = map $ENV{$_}, grep /^CHIMPS_(?:$variants).*_ROOT$/i, keys %ENV;
479 return $path if $path;
481 return $old_func->(@_);
485 sub bootstrap_plugins_db {
489 return unless $args{'plugins'};
493 my @plugins = @{ $args{'plugins'} };
494 foreach my $name ( @plugins ) {
495 my $plugin = RT::Plugin->new( name => $name );
496 Test::More::diag( "Initializing DB for the $name plugin" )
497 if $ENV{'TEST_VERBOSE'};
499 my $etc_path = $plugin->Path('etc');
500 Test::More::diag( "etc path of the plugin is '$etc_path'" )
501 if $ENV{'TEST_VERBOSE'};
503 unless ( -e $etc_path ) {
504 # We can't tell if the plugin has no data, or we screwed up the etc/ path
505 Test::More::ok(1, "There is no etc dir: no schema" );
506 Test::More::ok(1, "There is no etc dir: no ACLs" );
507 Test::More::ok(1, "There is no etc dir: no data" );
511 __reconnect_rt('as dba');
514 my ($ret, $msg) = $RT::Handle->InsertSchema( undef, $etc_path );
515 Test::More::ok($ret || $msg =~ /^Couldn't find schema/, "Created schema: ".($msg||''));
519 my ($ret, $msg) = $RT::Handle->InsertACL( undef, $etc_path );
520 Test::More::ok($ret || $msg =~ /^Couldn't find ACLs/, "Created ACL: ".($msg||''));
524 my $data_file = File::Spec->catfile( $etc_path, 'initialdata' );
525 if ( -e $data_file ) {
527 my ($ret, $msg) = $RT::Handle->InsertData( $data_file );;
528 Test::More::ok($ret, "Inserted data".($msg||''));
530 Test::More::ok(1, "There is no data file" );
537 my ($dsn, $user, $pass) = @_;
538 if ( $dsn =~ /Oracle/i ) {
539 $ENV{'NLS_LANG'} = "AMERICAN_AMERICA.AL32UTF8";
540 $ENV{'NLS_NCHAR'} = "AL32UTF8";
542 my $dbh = DBI->connect(
544 { RaiseError => 0, PrintError => 1 },
547 my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr;
548 print STDERR $msg; exit -1;
553 sub __create_database {
554 # bootstrap with dba cred
556 RT::Handle->SystemDSN,
557 $ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
560 unless ( $ENV{RT_TEST_PARALLEL} ) {
561 # already dropped db in parallel tests, need to do so for other cases.
562 __drop_database( $dbh );
565 RT::Handle->CreateDatabase( $dbh );
570 sub __drop_database {
573 # Pg doesn't like if you issue a DROP DATABASE while still connected
574 # it's still may fail if web-server is out there and holding a connection
577 my $my_dbh = $dbh? 0 : 1;
579 RT::Handle->SystemDSN,
580 $ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
583 # We ignore errors intentionally by not checking the return value of
584 # DropDatabase below, so let's also suppress DBI's printing of errors when
585 # we overzealously drop.
586 local $dbh->{PrintError} = 0;
587 local $dbh->{PrintWarn} = 0;
589 RT::Handle->DropDatabase( $dbh );
590 $dbh->disconnect if $my_dbh;
597 # look at %DBIHandle and $PrevHandle in DBIx::SB::Handle for explanation
598 $RT::Handle = RT::Handle->new;
599 $RT::Handle->dbh( undef );
600 $RT::Handle->Connect(
602 ? (User => $ENV{RT_DBA_USER}, Password => $ENV{RT_DBA_PASSWORD})
605 $RT::Handle->PrintError;
606 $RT::Handle->dbh->{PrintError} = 1;
607 return $RT::Handle->dbh;
610 sub __disconnect_rt {
611 # look at %DBIHandle and $PrevHandle in DBIx::SB::Handle for explanation
612 $RT::Handle->dbh->disconnect if $RT::Handle and $RT::Handle->dbh;
614 %DBIx::SearchBuilder::Handle::DBIHandle = ();
615 $DBIx::SearchBuilder::Handle::PrevHandle = undef;
619 delete $RT::System->{attributes};
621 DBIx::SearchBuilder::Record::Cachable->FlushCache
622 if DBIx::SearchBuilder::Record::Cachable->can("FlushCache");
628 # We use local to ensure that the $filter we grab is from InitLogging
629 # and not the handler generated by a previous call to this function
631 local $SIG{__WARN__};
633 $filter = $SIG{__WARN__};
635 $SIG{__WARN__} = sub {
637 my $status = $filter->(@_);
638 if ($status and $status eq 'IGNORE') {
639 return; # pretend the bad dream never happened
642 # Avoid reporting this anonymous call frame as the source of the warning.
643 goto &$Test_NoWarnings_Catcher;
650 =head2 load_or_create_user
654 sub load_or_create_user {
656 my %args = ( Privileged => 1, Disabled => 0, @_ );
658 my $MemberOf = delete $args{'MemberOf'};
659 $MemberOf = [ $MemberOf ] if defined $MemberOf && !ref $MemberOf;
662 my $obj = RT::User->new( RT->SystemUser );
663 if ( $args{'Name'} ) {
664 $obj->LoadByCols( Name => $args{'Name'} );
665 } elsif ( $args{'EmailAddress'} ) {
666 $obj->LoadByCols( EmailAddress => $args{'EmailAddress'} );
668 die "Name or EmailAddress is required";
672 $obj->SetPrivileged( $args{'Privileged'} || 0 )
673 if ($args{'Privileged'}||0) != ($obj->Privileged||0);
674 $obj->SetDisabled( $args{'Disabled'} || 0 )
675 if ($args{'Disabled'}||0) != ($obj->Disabled||0);
677 my ($val, $msg) = $obj->Create( %args );
678 die "$msg" unless $val;
681 # clean group membership
683 require RT::GroupMembers;
684 my $gms = RT::GroupMembers->new( RT->SystemUser );
685 my $groups_alias = $gms->Join(
686 FIELD1 => 'GroupId', TABLE2 => 'Groups', FIELD2 => 'id',
688 $gms->Limit( ALIAS => $groups_alias, FIELD => 'Domain', VALUE => 'UserDefined' );
689 $gms->Limit( FIELD => 'MemberId', VALUE => $obj->id );
690 while ( my $group_member_record = $gms->Next ) {
691 $group_member_record->Delete;
695 # add new user to groups
696 foreach ( @$MemberOf ) {
697 my $group = RT::Group->new( RT::SystemUser() );
698 $group->LoadUserDefinedGroup( $_ );
699 die "couldn't load group '$_'" unless $group->id;
700 $group->AddMember( $obj->id );
706 =head2 load_or_create_queue
710 sub load_or_create_queue {
712 my %args = ( Disabled => 0, @_ );
713 my $obj = RT::Queue->new( RT->SystemUser );
714 if ( $args{'Name'} ) {
715 $obj->LoadByCols( Name => $args{'Name'} );
717 die "Name is required";
719 unless ( $obj->id ) {
720 my ($val, $msg) = $obj->Create( %args );
721 die "$msg" unless $val;
723 my @fields = qw(CorrespondAddress CommentAddress);
724 foreach my $field ( @fields ) {
725 next unless exists $args{ $field };
726 next if $args{ $field } eq ($obj->$field || '');
728 no warnings 'uninitialized';
729 my $method = 'Set'. $field;
730 my ($val, $msg) = $obj->$method( $args{ $field } );
731 die "$msg" unless $val;
738 sub delete_queue_watchers {
742 foreach my $q ( @queues ) {
743 foreach my $t (qw(Cc AdminCc) ) {
744 $q->DeleteWatcher( Type => $t, PrincipalId => $_->MemberId )
745 foreach @{ $q->$t()->MembersObj->ItemsArrayRef };
751 local $Test::Builder::Level = $Test::Builder::Level + 1;
754 my $defaults = shift;
756 @data = sort { rand(100) <=> rand(100) } @data
757 if delete $defaults->{'RandomOrder'};
759 $defaults->{'Queue'} ||= 'General';
763 my %args = %{ shift @data };
764 $args{$_} = $res[ $args{$_} ]->id foreach
765 grep $args{ $_ }, keys %RT::Ticket::LINKTYPEMAP;
766 push @res, $self->create_ticket( %$defaults, %args );
772 local $Test::Builder::Level = $Test::Builder::Level + 1;
777 if ($args{Queue} && $args{Queue} =~ /\D/) {
778 my $queue = RT::Queue->new(RT->SystemUser);
779 if (my $id = $queue->Load($args{Queue}) ) {
782 die ("Error: Invalid queue $args{Queue}");
786 if ( my $content = delete $args{'Content'} ) {
787 $args{'MIMEObj'} = MIME::Entity->build(
788 From => $args{'Requestor'},
789 Subject => $args{'Subject'},
794 my $ticket = RT::Ticket->new( RT->SystemUser );
795 my ( $id, undef, $msg ) = $ticket->Create( %args );
796 Test::More::ok( $id, "ticket created" )
797 or Test::More::diag("error: $msg");
799 # hackish, but simpler
800 if ( $args{'LastUpdatedBy'} ) {
801 $ticket->__Set( Field => 'LastUpdatedBy', Value => $args{'LastUpdatedBy'} );
805 for my $field ( keys %args ) {
806 #TODO check links and watchers
808 if ( $field =~ /CustomField-(\d+)/ ) {
810 my $got = join ',', sort map $_->Content,
811 @{ $ticket->CustomFieldValues($cf)->ItemsArrayRef };
812 my $expected = ref $args{$field}
813 ? join( ',', sort @{ $args{$field} } )
815 Test::More::is( $got, $expected, 'correct CF values' );
818 next if ref $args{$field};
819 next unless $ticket->can($field) or $ticket->_Accessible($field,"read");
820 next if ref $ticket->$field();
821 Test::More::is( $ticket->$field(), $args{$field}, "$field is correct" );
831 my $tickets = RT::Tickets->new( RT->SystemUser );
833 $tickets->FromSQL( $query );
838 while ( my $ticket = $tickets->Next ) {
843 =head2 load_or_create_custom_field
847 sub load_or_create_custom_field {
849 my %args = ( Disabled => 0, @_ );
850 my $obj = RT::CustomField->new( RT->SystemUser );
851 if ( $args{'Name'} ) {
852 $obj->LoadByName( Name => $args{'Name'}, Queue => $args{'Queue'} );
854 die "Name is required";
856 unless ( $obj->id ) {
857 my ($val, $msg) = $obj->Create( %args );
858 die "$msg" unless $val;
867 $current = $current ? RT::CurrentUser->new($current) : RT->SystemUser;
868 my $tickets = RT::Tickets->new( $current );
869 $tickets->OrderBy( FIELD => 'id', ORDER => 'DESC' );
870 $tickets->Limit( FIELD => 'id', OPERATOR => '>', VALUE => '0' );
871 $tickets->RowsPerPage( 1 );
872 return $tickets->First;
880 RT::ACE->new( RT->SystemUser );
881 my @fields = keys %{ RT::ACE->_ClassAccessible };
884 my $acl = RT::ACL->new( RT->SystemUser );
885 $acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
888 while ( my $ace = $acl->Next ) {
889 my $obj = $ace->PrincipalObj->Object;
890 if ( $obj->isa('RT::Group') && $obj->Type eq 'UserEquiv' && $obj->Instance == RT->Nobody->id ) {
895 foreach my $field( @fields ) {
896 $tmp{ $field } = $ace->__Value( $field );
906 foreach my $entry ( @entries ) {
907 my $ace = RT::ACE->new( RT->SystemUser );
908 my ($status, $msg) = $ace->RT::Record::Create( %$entry );
910 Test::More::diag "couldn't create a record: $msg";
919 my $acl = RT::ACL->new( RT->SystemUser );
920 $acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
921 while ( my $ace = $acl->Next ) {
922 my $obj = $ace->PrincipalObj->Object;
923 if ( $obj->isa('RT::Group') && $obj->Type eq 'UserEquiv' && $obj->Instance == RT->Nobody->id ) {
928 return $self->add_rights( @_ );
933 my @list = ref $_[0]? @_: @_? { @_ }: ();
936 foreach my $e (@list) {
937 my $principal = delete $e->{'Principal'};
938 unless ( ref $principal ) {
939 if ( $principal =~ /^(everyone|(?:un)?privileged)$/i ) {
940 $principal = RT::Group->new( RT->SystemUser );
941 $principal->LoadSystemInternalGroup($1);
942 } elsif ( $principal =~ /^(Owner|Requestor|(?:Admin)?Cc)$/i ) {
943 $principal = RT::Group->new( RT->SystemUser );
944 $principal->LoadByCols(
945 Domain => (ref($e->{'Object'})||'RT::System').'-Role',
947 ref($e->{'Object'})? (Instance => $e->{'Object'}->id): (),
950 die "principal is not an object, but also is not name of a system group";
953 unless ( $principal->isa('RT::Principal') ) {
954 if ( $principal->can('PrincipalObj') ) {
955 $principal = $principal->PrincipalObj;
958 my @rights = ref $e->{'Right'}? @{ $e->{'Right'} }: ($e->{'Right'});
959 foreach my $right ( @rights ) {
960 my ($status, $msg) = $principal->GrantRight( %$e, Right => $right );
961 $RT::Logger->debug($msg);
970 require RT::Test::Web;
972 url => RT::Test::Web->rt_base_url,
974 action => 'correspond',
977 command => $RT::BinPath .'/rt-mailgate',
980 my $message = delete $args{'message'};
982 $args{after_open} = sub {
983 my $child_in = shift;
984 if ( UNIVERSAL::isa($message, 'MIME::Entity') ) {
985 $message->print( $child_in );
987 print $child_in $message;
991 $self->run_and_capture(%args);
994 sub run_and_capture {
998 my $after_open = delete $args{after_open};
1000 my $cmd = delete $args{'command'};
1001 die "Couldn't find command ($cmd)" unless -f $cmd;
1003 $cmd .= ' --debug' if delete $args{'debug'};
1005 while( my ($k,$v) = each %args ) {
1007 $cmd .= " --$k '$v'";
1011 DBIx::SearchBuilder::Record::Cachable->FlushCache;
1014 my ($child_out, $child_in);
1015 my $pid = IPC::Open2::open2($child_out, $child_in, $cmd);
1017 $after_open->($child_in, $child_out) if $after_open;
1021 my $result = do { local $/; <$child_out> };
1024 return ($?, $result);
1027 sub send_via_mailgate_and_http {
1029 my $message = shift;
1032 my ($status, $gate_result) = $self->run_mailgate(
1033 message => $message, %args
1037 unless ( $status >> 8 ) {
1038 ($id) = ($gate_result =~ /Ticket:\s*(\d+)/i);
1040 Test::More::diag "Couldn't find ticket id in text:\n$gate_result"
1041 if $ENV{'TEST_VERBOSE'};
1044 Test::More::diag "Mailgate output:\n$gate_result"
1045 if $ENV{'TEST_VERBOSE'};
1047 return ($status, $id);
1051 sub send_via_mailgate {
1053 my $message = shift;
1054 my %args = ( action => 'correspond',
1059 if ( UNIVERSAL::isa( $message, 'MIME::Entity' ) ) {
1060 $message = $message->as_string;
1063 my ( $status, $error_message, $ticket )
1064 = RT::Interface::Email::Gateway( {%args, message => $message} );
1065 return ( $status, $ticket ? $ticket->id : 0 );
1070 sub open_mailgate_ok {
1072 my $baseurl = shift;
1073 my $queue = shift || 'general';
1074 my $action = shift || 'correspond';
1075 Test::More::ok(open(my $mail, '|-', "$RT::BinPath/rt-mailgate --url $baseurl --queue $queue --action $action"), "Opened the mailgate - $!");
1080 sub close_mailgate_ok {
1084 Test::More::is ($? >> 8, 0, "The mail gateway exited normally. yay");
1089 my $expected = shift;
1091 my $mailsent = scalar grep /\S/, split /%% split me! %%\n/,
1092 RT::Test->file_content(
1099 $mailsent, $expected,
1100 "The number of mail sent ($expected) matches. yay"
1104 sub fetch_caught_mails {
1106 return grep /\S/, split /%% split me! %%\n/,
1107 RT::Test->file_content(
1114 sub clean_caught_mails {
1115 unlink $tmp{'mailbox'};
1118 =head2 get_relocatable_dir
1120 Takes a path relative to the location of the test file that is being
1121 run and returns a path that takes the invocation path into account.
1123 e.g. C<RT::Test::get_relocatable_dir(File::Spec->updir(), 'data', 'emails')>
1125 Parent directory traversals (C<..> or File::Spec->updir()) are naively
1126 canonicalized based on the test file path (C<$0>) so that symlinks aren't
1127 followed. This is the exact opposite behaviour of most filesystems and is
1128 considered "wrong", however it is necessary for some subsets of tests which are
1129 symlinked into the testing tree.
1133 sub get_relocatable_dir {
1134 my @directories = File::Spec->splitdir(
1135 File::Spec->rel2abs((File::Spec->splitpath($0))[1])
1137 push @directories, File::Spec->splitdir($_) for @_;
1140 for (@directories) {
1141 if ($_ eq "..") { pop @clean }
1142 elsif ($_ ne ".") { push @clean, $_ }
1144 return File::Spec->catdir(@clean);
1147 =head2 get_relocatable_file
1149 Same as get_relocatable_dir, but takes a file and a path instead
1152 e.g. RT::Test::get_relocatable_file('test-email',
1153 (File::Spec->updir(), 'data', 'emails'))
1157 sub get_relocatable_file {
1159 return File::Spec->catfile(get_relocatable_dir(@_), $file);
1162 sub get_abs_relocatable_dir {
1163 (my $volume, my $directories, my $file) = File::Spec->splitpath($0);
1164 if (File::Spec->file_name_is_absolute($directories)) {
1165 return File::Spec->catdir($directories, @_);
1167 return File::Spec->catdir(Cwd->getcwd(), $directories, @_);
1174 DIR => $tmp{directory},
1179 sub import_gnupg_key {
1182 my $type = shift || 'secret';
1184 $key =~ s/\@/-at-/g;
1185 $key .= ".$type.key";
1187 require RT::Crypt::GnuPG;
1189 # simple strategy find data/gnupg/keys, from the dir where test file lives
1190 # to updirs, try 3 times in total
1191 my $path = File::Spec->catfile( 'data', 'gnupg', 'keys' );
1193 for my $up ( 0 .. 2 ) {
1194 my $p = get_relocatable_dir($path);
1200 $path = File::Spec->catfile( File::Spec->updir(), $path );
1204 die "can't find the dir where gnupg keys are stored"
1207 return RT::Crypt::GnuPG::ImportKey(
1208 RT::Test->file_content( [ $abs_path, $key ] ) );
1212 sub lsign_gnupg_key {
1216 require RT::Crypt::GnuPG; require GnuPG::Interface;
1217 my $gnupg = GnuPG::Interface->new();
1218 my %opt = RT->Config->Get('GnuPGOptions');
1219 $gnupg->options->hash_init(
1220 RT::Crypt::GnuPG::_PrepareGnuPGOptions( %opt ),
1221 meta_interactive => 0,
1225 my $handles = GnuPG::Handles->new(
1226 stdin => ($handle{'input'} = IO::Handle->new()),
1227 stdout => ($handle{'output'} = IO::Handle->new()),
1228 stderr => ($handle{'error'} = IO::Handle->new()),
1229 logger => ($handle{'logger'} = IO::Handle->new()),
1230 status => ($handle{'status'} = IO::Handle->new()),
1231 command => ($handle{'command'} = IO::Handle->new()),
1235 local $SIG{'CHLD'} = 'DEFAULT';
1236 local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
1237 my $pid = $gnupg->wrap_call(
1238 handles => $handles,
1239 commands => ['--lsign-key'],
1240 command_args => [$key],
1242 close $handle{'input'};
1243 while ( my $str = readline $handle{'status'} ) {
1244 if ( $str =~ /^\[GNUPG:\]\s*GET_BOOL sign_uid\..*/ ) {
1245 print { $handle{'command'} } "y\n";
1251 close $handle{'output'};
1254 $res{'exit_code'} = $?;
1255 foreach ( qw(error logger status) ) {
1256 $res{$_} = do { local $/; readline $handle{$_} };
1257 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1260 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1261 $RT::Logger->warning( $res{'error'} ) if $res{'error'};
1262 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1263 if ( $err || $res{'exit_code'} ) {
1264 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
1269 sub trust_gnupg_key {
1273 require RT::Crypt::GnuPG; require GnuPG::Interface;
1274 my $gnupg = GnuPG::Interface->new();
1275 my %opt = RT->Config->Get('GnuPGOptions');
1276 $gnupg->options->hash_init(
1277 RT::Crypt::GnuPG::_PrepareGnuPGOptions( %opt ),
1278 meta_interactive => 0,
1282 my $handles = GnuPG::Handles->new(
1283 stdin => ($handle{'input'} = IO::Handle->new()),
1284 stdout => ($handle{'output'} = IO::Handle->new()),
1285 stderr => ($handle{'error'} = IO::Handle->new()),
1286 logger => ($handle{'logger'} = IO::Handle->new()),
1287 status => ($handle{'status'} = IO::Handle->new()),
1288 command => ($handle{'command'} = IO::Handle->new()),
1292 local $SIG{'CHLD'} = 'DEFAULT';
1293 local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
1294 my $pid = $gnupg->wrap_call(
1295 handles => $handles,
1296 commands => ['--edit-key'],
1297 command_args => [$key],
1299 close $handle{'input'};
1302 while ( my $str = readline $handle{'status'} ) {
1303 if ( $str =~ /^\[GNUPG:\]\s*\QGET_LINE keyedit.prompt/ ) {
1305 print { $handle{'command'} } "quit\n";
1307 print { $handle{'command'} } "trust\n";
1309 } elsif ( $str =~ /^\[GNUPG:\]\s*\QGET_LINE edit_ownertrust.value/ ) {
1310 print { $handle{'command'} } "5\n";
1311 } elsif ( $str =~ /^\[GNUPG:\]\s*\QGET_BOOL edit_ownertrust.set_ultimate.okay/ ) {
1312 print { $handle{'command'} } "y\n";
1319 close $handle{'output'};
1322 $res{'exit_code'} = $?;
1323 foreach ( qw(error logger status) ) {
1324 $res{$_} = do { local $/; readline $handle{$_} };
1325 delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
1328 $RT::Logger->debug( $res{'status'} ) if $res{'status'};
1329 $RT::Logger->warning( $res{'error'} ) if $res{'error'};
1330 $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
1331 if ( $err || $res{'exit_code'} ) {
1332 $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
1340 require RT::Test::Web;
1342 if ($rttest_opt{nodb} and not $rttest_opt{server_ok}) {
1343 die "You are trying to use a test web server without a database. "
1344 ."You may want noinitialdata => 1 instead. "
1345 ."Pass server_ok => 1 if you know what you're doing.";
1349 $ENV{'RT_TEST_WEB_HANDLER'} = undef
1350 if $rttest_opt{actual_server} && ($ENV{'RT_TEST_WEB_HANDLER'}||'') eq 'inline';
1351 $ENV{'RT_TEST_WEB_HANDLER'} ||= 'plack';
1352 my $which = $ENV{'RT_TEST_WEB_HANDLER'};
1353 my ($server, $variant) = split /\+/, $which, 2;
1355 my $function = 'start_'. $server .'_server';
1356 unless ( $self->can($function) ) {
1357 die "Don't know how to start server '$server'";
1359 return $self->$function( variant => $variant, @_ );
1364 my %server_opt = @_;
1369 open( my $warn_fh, ">", \$warnings );
1370 local *STDERR = $warn_fh;
1372 if ($server_opt{variant} and $server_opt{variant} eq 'rt-server') {
1374 my $file = "$RT::SbinPath/rt-server";
1375 my $psgi = do $file;
1377 die "Couldn't parse $file: $@" if $@;
1378 die "Couldn't do $file: $!" unless defined $psgi;
1379 die "Couldn't run $file" unless $psgi;
1384 require RT::Interface::Web::Handler;
1385 $app = RT::Interface::Web::Handler->PSGIApp;
1388 require Plack::Middleware::Test::StashWarnings;
1389 my $stashwarnings = Plack::Middleware::Test::StashWarnings->new;
1390 $app = $stashwarnings->wrap($app);
1392 if ($server_opt{basic_auth}) {
1393 require Plack::Middleware::Auth::Basic;
1394 $app = Plack::Middleware::Auth::Basic->wrap(
1396 authenticator => sub {
1397 my ($username, $password) = @_;
1398 return $username eq 'root' && $password eq 'password';
1404 $stashwarnings->add_warning( $warnings ) if $warnings;
1409 sub start_plack_server {
1412 require Plack::Loader;
1413 my $plack_server = Plack::Loader->load
1416 server_ready => sub {
1417 kill 'USR1' => getppid();
1420 # We are expecting a USR1 from the child process after it's ready
1421 # to listen. We set this up _before_ we fork to avoid race
1424 local $SIG{USR1} = sub { $handled = 1};
1428 die "failed to fork" unless defined $pid;
1431 sleep 15 unless $handled;
1432 Test::More::diag "did not get expected USR1 for test server readiness"
1434 push @SERVERS, $pid;
1435 my $Tester = Test::Builder->new;
1436 $Tester->ok(1, "started plack server ok");
1439 unless $rttest_opt{nodb};
1440 return ("http://localhost:$port", RT::Test::Web->new);
1444 if ( $^O !~ /MSWin32/ ) {
1446 or die "Can't start a new session: $!";
1449 # stick this in a scope so that when $app is garbage collected,
1450 # StashWarnings can complain about unhandled warnings
1452 $plack_server->run($self->test_app(@_));
1459 sub start_inline_server {
1462 require Test::WWW::Mechanize::PSGI;
1463 unshift @RT::Test::Web::ISA, 'Test::WWW::Mechanize::PSGI';
1465 # Clear out squished CSS and JS cache, since it's retained across
1466 # servers, since it's in-process
1467 RT::Interface::Web->ClearSquished;
1468 require RT::Interface::Web::Request;
1469 RT::Interface::Web::Request->clear_callback_cache;
1471 Test::More::ok(1, "psgi test server ok");
1472 $TEST_APP = $self->test_app(@_);
1473 return ("http://localhost:$port", RT::Test::Web->new);
1476 sub start_apache_server {
1478 my %server_opt = @_;
1479 $server_opt{variant} ||= 'mod_perl';
1480 $ENV{RT_TEST_WEB_HANDLER} = "apache+$server_opt{variant}";
1482 require RT::Test::Apache;
1483 my $pid = RT::Test::Apache->start_server(
1488 push @SERVERS, $pid;
1490 my $url = RT->Config->Get('WebURL');
1492 return ($url, RT::Test::Web->new);
1498 return unless @SERVERS;
1501 $sig = 'INT' if $ENV{'RT_TEST_WEB_HANDLER'} eq "plack";
1502 kill $sig, @SERVERS;
1503 foreach my $pid (@SERVERS) {
1504 if ($ENV{RT_TEST_WEB_HANDLER} =~ /^apache/) {
1505 sleep 1 while kill 0, $pid;
1514 sub temp_directory {
1515 return $tmp{'directory'};
1523 $path = File::Spec->catfile( @$path ) if ref $path eq 'ARRAY';
1525 Test::More::diag "reading content of '$path'" if $ENV{'TEST_VERBOSE'};
1527 open( my $fh, "<:raw", $path )
1529 warn "couldn't open file '$path': $!" unless $args{noexist};
1532 my $content = do { local $/; <$fh> };
1535 unlink $path if $args{'unlink'};
1540 sub find_executable {
1545 foreach my $dir ( split /:/, $ENV{'PATH'} ) {
1546 my $fpath = File::Spec->catpath(
1547 (File::Spec->splitpath( $dir, 'no file' ))[0..1], $name
1549 next unless -e $fpath && -r _ && -x _;
1556 return unless $ENV{RT_TEST_VERBOSE} || $ENV{TEST_VERBOSE};
1557 goto \&Test::More::diag;
1562 require RT::EmailParser;
1563 my $parser = RT::EmailParser->new;
1564 $parser->ParseMIMEEntityFromScalar( $mail );
1565 return $parser->Entity;
1569 Test::More::ok($_[0], $_[1] || 'This works');
1573 Test::More::ok(!$_[0], $_[1] || 'This should fail');
1577 my ($cmd, @args) = @_;
1578 my $builder = RT::Test->builder;
1580 if ($cmd eq "skip_all") {
1581 $check_warnings_in_end = 0;
1582 } elsif ($cmd eq "tests") {
1583 # Increment the test count for the warnings check
1586 $builder->plan($cmd, @args);
1590 my $builder = RT::Test->builder;
1592 Test::NoWarnings::had_no_warnings();
1593 $check_warnings_in_end = 0;
1595 $builder->done_testing(@_);
1599 my $Test = RT::Test->builder;
1600 return if $Test->{Original_Pid} != $$;
1602 # we are in END block and should protect our exit code
1603 # so calls below may call system or kill that clobbers $?
1606 Test::NoWarnings::had_no_warnings() if $check_warnings_in_end;
1608 RT::Test->stop_server(1);
1611 if ( !$Test->is_passing ) {
1612 $tmp{'directory'}->unlink_on_destroy(0);
1615 "Some tests failed or we bailed out, tmp directory"
1616 ." '$tmp{directory}' is not cleaned"
1620 if ( $ENV{RT_TEST_PARALLEL} && $created_new_db ) {
1624 # Drop our port from t/tmp/ports; do this after dropping the
1625 # database, as our port lock is also a lock on the database name.
1628 my $portfile = "$tmp{'directory'}/../ports";
1629 sysopen(PORTS, $portfile, O_RDWR|O_CREAT)
1630 or die "Can't write to ports file $portfile: $!";
1631 flock(PORTS, LOCK_EX)
1632 or die "Can't write-lock ports file $portfile: $!";
1633 $ports{$_}++ for split ' ', join("",<PORTS>);
1634 delete $ports{$port};
1637 print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
1638 close(PORTS) or die "Can't close ports file: $!";
1643 # ease the used only once warning
1646 %{'RT::I18N::en_us::Lexicon'};
1647 %{'Win32::Locale::Lexicon'};