diff options
author | ivan <ivan> | 2009-12-31 13:16:41 +0000 |
---|---|---|
committer | ivan <ivan> | 2009-12-31 13:16:41 +0000 |
commit | b4b0c7e72d7eaee2fbfc7022022c9698323203dd (patch) | |
tree | ba4cd21399e412c32fe3737eaa8478e3271509f9 /rt/t | |
parent | 2dfda73eeb3eae2d4f894099754794ef07d060dd (diff) |
import rt 3.8.7
Diffstat (limited to 'rt/t')
200 files changed, 24055 insertions, 0 deletions
diff --git a/rt/t/00-compile.t b/rt/t/00-compile.t new file mode 100644 index 000000000..7257c3e8c --- /dev/null +++ b/rt/t/00-compile.t @@ -0,0 +1,58 @@ + +use strict; +use warnings; + +use RT::Test nodata => 1, tests => 34; + +require_ok("RT"); +require_ok("RT::Test"); +require_ok("RT::ACL"); +require_ok("RT::Handle"); +require_ok("RT::Transaction"); +require_ok("RT::Interface::CLI"); +require_ok("RT::Interface::Email"); +require_ok("RT::Links"); +require_ok("RT::Queues"); +require_ok("RT::Scrips"); +require_ok("RT::Templates"); +require_ok("RT::Principals"); +require_ok("RT::Attachments"); +require_ok("RT::GroupMember"); +require_ok("RT::ScripAction"); +require_ok("RT::CustomFields"); +require_ok("RT::GroupMembers"); +require_ok("RT::ScripActions"); +require_ok("RT::Transactions"); +require_ok("RT::ScripCondition"); +require_ok("RT::Action::Generic"); +require_ok("RT::ScripConditions"); +require_ok("RT::Search::Generic"); +require_ok("RT::Search::Generic"); +require_ok("RT::Search::Generic"); +require_ok("RT::Search::Generic"); +require_ok("RT::Action::SendEmail"); +require_ok("RT::CachedGroupMembers"); +require_ok("RT::Condition::Generic"); +require_ok("RT::Interface::Web"); +require_ok("RT::SavedSearch"); +require_ok("RT::SavedSearches"); +require_ok("RT::Installer"); +require_ok("RT::Util"); + + +# no the following doesn't work yet +__END__ +use File::Find::Rule; + +my @files = File::Find::Rule->file() + ->name( '*.pm' ) + ->in( 'lib' ); + +plan tests => scalar @files; + +for (@files) { + local $SIG{__WARN__} = sub {}; + require_ok($_); +} + +1; diff --git a/rt/t/00-mason-syntax.t b/rt/t/00-mason-syntax.t new file mode 100644 index 000000000..0584f630f --- /dev/null +++ b/rt/t/00-mason-syntax.t @@ -0,0 +1,44 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use RT::Test tests => 1; + +my $ok = 1; + +use File::Find; +find( { + no_chdir => 1, + wanted => sub { + return if /(?:\.(?:jpe?g|png|gif|rej)|\~)$/i; + return if m{/\.[^/]+\.swp$}; # vim swap files + return unless -f $_; + diag "testing $_" if $ENV{'TEST_VERBOSE'}; + eval { compile_file($_) } and return; + $ok = 0; + diag "error in ${File::Find::name}:\n$@"; + }, +}, RT::Test::get_relocatable_dir('../share/html')); +ok($ok, "mason syntax is ok"); + +use HTML::Mason; +use HTML::Mason::Compiler; +use HTML::Mason::Compiler::ToObject; +BEGIN { require RT::Test; } +use Encode qw(decode_utf8); + +sub compile_file { + my $file = shift; + + my $text = decode_utf8(RT::Test->file_content($file)); + + my $compiler = new HTML::Mason::Compiler::ToObject; + $compiler->compile( + comp_source => $text, + name => 'my', + $HTML::Mason::VERSION >= 1.36? (comp_path => 'my'): (), + ); + return 1; +} + diff --git a/rt/t/api/ace.t b/rt/t/api/ace.t new file mode 100644 index 000000000..4031046d9 --- /dev/null +++ b/rt/t/api/ace.t @@ -0,0 +1,238 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 76; + + +{ + +ok(require RT::ACE); + + +} + +{ + +my $Queue = RT::Queue->new($RT::SystemUser); + +is ($Queue->AvailableRights->{'DeleteTicket'} , 'Delete tickets', "Found the delete ticket right"); +is ($RT::System->AvailableRights->{'SuperUser'}, 'Do anything and everything', "Found the superuser right"); + + + +} + +{ + +use_ok('RT::User'); +my $user_a = RT::User->new($RT::SystemUser); +$user_a->Create( Name => 'DelegationA', Privileged => 1); +ok ($user_a->Id, "Created delegation user a"); + +my $user_b = RT::User->new($RT::SystemUser); +$user_b->Create( Name => 'DelegationB', Privileged => 1); +ok ($user_b->Id, "Created delegation user b"); + + +use_ok('RT::Queue'); +my $q = RT::Queue->new($RT::SystemUser); +$q->Create(Name =>'DelegationTest'); +ok ($q->Id, "Created a delegation test queue"); + + +#------ First, we test whether a user can delegate a right that's been granted to him personally +my ($val, $msg) = $user_a->PrincipalObj->GrantRight(Object => $RT::System, Right => 'AdminOwnPersonalGroups'); +ok($val, $msg); + +($val, $msg) = $user_a->PrincipalObj->GrantRight(Object =>$q, Right => 'OwnTicket'); +ok($val, $msg); + +ok($user_a->HasRight( Object => $RT::System, Right => 'AdminOwnPersonalGroups') ,"user a has the right 'AdminOwnPersonalGroups' directly"); + +my $a_delegates = RT::Group->new($user_a); +$a_delegates->CreatePersonalGroup(Name => 'Delegates'); +ok( $a_delegates->Id ,"user a creates a personal group 'Delegates'"); +ok( $a_delegates->AddMember($user_b->PrincipalId) ,"user a adds user b to personal group 'delegates'"); + +ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to OwnTicket' in queue 'DelegationTest'"); +ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a has the right to 'OwnTicket' in queue 'DelegationTest'"); +ok(!$user_a->HasRight( Object => $RT::System, Right => 'DelegateRights') ,"user a does not have the right 'delegate rights'"); + + +my $own_ticket_ace = RT::ACE->new($user_a); +my $user_a_equiv_group = RT::Group->new($user_a); +$user_a_equiv_group->LoadACLEquivalenceGroup($user_a->PrincipalObj); +ok ($user_a_equiv_group->Id, "Loaded the user A acl equivalence group"); +my $user_b_equiv_group = RT::Group->new($user_b); +$user_b_equiv_group->LoadACLEquivalenceGroup($user_b->PrincipalObj); +ok ($user_b_equiv_group->Id, "Loaded the user B acl equivalence group"); +$own_ticket_ace->LoadByValues( PrincipalType => 'Group', PrincipalId => $user_a_equiv_group->PrincipalId, Object=>$q, RightName => 'OwnTicket'); + +ok ($own_ticket_ace->Id, "Found the ACE we want to test with for now"); + + +($val, $msg) = $own_ticket_ace->Delegate(PrincipalId => $a_delegates->PrincipalId) ; +ok( !$val ,"user a tries and fails to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg"); + + +($val, $msg) = $user_a->PrincipalObj->GrantRight( Right => 'DelegateRights'); +ok($val, "user a is granted the right to 'delegate rights' - $msg"); + +ok($user_a->HasRight( Object => $RT::System, Right => 'DelegateRights') ,"user a has the right 'DeletgateRights'"); + +($val, $msg) = $own_ticket_ace->Delegate(PrincipalId => $a_delegates->PrincipalId) ; + +ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg"); +ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'"); +my $delegated_ace = RT::ACE->new($user_a); +$delegated_ace->LoadByValues ( Object => $q, RightName => 'OwnTicket', PrincipalType => 'Group', +PrincipalId => $a_delegates->PrincipalId, DelegatedBy => $user_a->PrincipalId, DelegatedFrom => $own_ticket_ace->Id); +ok ($delegated_ace->Id, "Found the delegated ACE"); + +ok( $a_delegates->DeleteMember($user_b->PrincipalId) ,"user a removes b from pg 'delegates'"); +ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'"); +ok( $a_delegates->AddMember($user_b->PrincipalId) ,"user a adds user b to personal group 'delegates'"); +ok( $user_b->HasRight(Right => 'OwnTicket', Object=> $q) ,"user b has the right to own tickets in queue 'DelegationTest'"); +ok( $delegated_ace->Delete ,"user a revokes pg 'delegates' right to 'OwnTickets' in queue 'DelegationTest'"); +ok( ! $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'"); + +($val, $msg) = $own_ticket_ace->Delegate(PrincipalId => $a_delegates->PrincipalId) ; +ok( $val ,"user a delegates pg 'delegates' right to 'OwnTickets' in queue 'DelegationTest' - $msg"); + +ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'"); + +($val, $msg) = $user_a->PrincipalObj->RevokeRight(Object=>$q, Right => 'OwnTicket'); +ok($val, "Revoked user a's right to own tickets in queue 'DelegationTest". $msg); + +ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does not have the right to own tickets in queue 'DelegationTest'"); + + ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'"); + +($val, $msg) = $user_a->PrincipalObj->GrantRight(Object=>$q, Right => 'OwnTicket'); +ok($val, $msg); + + ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a has the right to own tickets in queue 'DelegationTest'"); + + ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'"); + +# {{{ get back to a known clean state +($val, $msg) = $user_a->PrincipalObj->RevokeRight( Object => $q, Right => 'OwnTicket'); +ok($val, "Revoked user a's right to own tickets in queue 'DelegationTest -". $msg); +ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"make sure that user a can't own tickets in queue 'DelegationTest'"); +# }}} + + +# {{{ Set up some groups and membership +my $del1 = RT::Group->new($RT::SystemUser); +($val, $msg) = $del1->CreateUserDefinedGroup(Name => 'Del1'); +ok( $val ,"create a group del1 - $msg"); + +my $del2 = RT::Group->new($RT::SystemUser); +($val, $msg) = $del2->CreateUserDefinedGroup(Name => 'Del2'); +ok( $val ,"create a group del2 - $msg"); +($val, $msg) = $del1->AddMember($del2->PrincipalId); +ok( $val,"make del2 a member of del1 - $msg"); + +my $del2a = RT::Group->new($RT::SystemUser); +($val, $msg) = $del2a->CreateUserDefinedGroup(Name => 'Del2a'); +ok( $val ,"create a group del2a - $msg"); +($val, $msg) = $del2->AddMember($del2a->PrincipalId); +ok($val ,"make del2a a member of del2 - $msg"); + +my $del2b = RT::Group->new($RT::SystemUser); +($val, $msg) = $del2b->CreateUserDefinedGroup(Name => 'Del2b'); +ok( $val ,"create a group del2b - $msg"); +($val, $msg) = $del2->AddMember($del2b->PrincipalId); +ok($val ,"make del2b a member of del2 - $msg"); + +($val, $msg) = $del2->AddMember($user_a->PrincipalId) ; +ok($val,"make 'user a' a member of del2 - $msg"); + +($val, $msg) = $del2b->AddMember($user_a->PrincipalId) ; +ok($val,"make 'user a' a member of del2b - $msg"); + +# }}} + +# {{{ Grant a right to a group and make sure that a submember can delegate the right and that it does not get yanked +# when a user is removed as a submember, when they're a submember through another path +($val, $msg) = $del1->PrincipalObj->GrantRight( Object=> $q, Right => 'OwnTicket'); +ok( $val ,"grant del1 the right to 'OwnTicket' in queue 'DelegationTest' - $msg"); + +ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"make sure that user a can own tickets in queue 'DelegationTest'"); + +my $group_ace= RT::ACE->new($user_a); +$group_ace->LoadByValues( PrincipalType => 'Group', PrincipalId => $del1->PrincipalId, Object => $q, RightName => 'OwnTicket'); + +ok ($group_ace->Id, "Found the ACE we want to test with for now"); + +($val, $msg) = $group_ace->Delegate(PrincipalId => $a_delegates->PrincipalId); + +ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg"); +ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'"); + + +($val, $msg) = $del2b->DeleteMember($user_a->PrincipalId); +ok( $val ,"remove user a from group del2b - $msg"); +ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a has the right to own tickets in queue 'DelegationTest'"); +ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'"); + +# }}} + +# {{{ When a user is removed froom a group by the only path they're in there by, make sure the delegations go away +($val, $msg) = $del2->DeleteMember($user_a->PrincipalId); +ok( $val ,"remove user a from group del2 - $msg"); +ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does not have the right to own tickets in queue 'DelegationTest' "); +ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest' "); +# }}} + +($val, $msg) = $del2->AddMember($user_a->PrincipalId); +ok( $val ,"make user a a member of group del2 - $msg"); + +($val, $msg) = $del2->PrincipalObj->GrantRight(Object=>$q, Right => 'OwnTicket'); +ok($val, "grant the right 'own tickets' in queue 'DelegationTest' to group del2 - $msg"); + +my $del2_right = RT::ACE->new($user_a); +$del2_right->LoadByValues( PrincipalId => $del2->PrincipalId, PrincipalType => 'Group', Object => $q, RightName => 'OwnTicket'); +ok ($del2_right->Id, "Found the right"); + +($val, $msg) = $del2_right->Delegate(PrincipalId => $a_delegates->PrincipalId); +ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' gotten via del2 to personal group 'delegates' - $msg"); + +# They have it via del1 and del2 +ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'"); + + +($val, $msg) = $del2->PrincipalObj->RevokeRight(Object=>$q, Right => 'OwnTicket'); +ok($val, "revoke the right 'own tickets' in queue 'DelegationTest' to group del2 - $msg"); +ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does has the right to own tickets in queue 'DelegationTest' via del1"); +ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'"); + +($val, $msg) = $del2->PrincipalObj->GrantRight(Object=>$q, Right => 'OwnTicket'); +ok($val, "grant the right 'own tickets' in queue 'DelegationTest' to group del2 - $msg"); + + +$group_ace= RT::ACE->new($user_a); +$group_ace->LoadByValues( PrincipalType => 'Group', PrincipalId => $del1->PrincipalId, Object=>$q, RightName => 'OwnTicket'); + +ok ($group_ace->Id, "Found the ACE we want to test with for now"); + +($val, $msg) = $group_ace->Delegate(PrincipalId => $a_delegates->PrincipalId); + +ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg"); + +ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'"); + +($val, $msg) = $del2->DeleteMember($user_a->PrincipalId); +ok( $val ,"remove user a from group del2 - $msg"); + +ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does not have the right to own tickets in queue 'DelegationTest'"); + +ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'"); + + + + +} + +1; diff --git a/rt/t/api/action-createtickets.t b/rt/t/api/action-createtickets.t new file mode 100644 index 000000000..69ceb8d4d --- /dev/null +++ b/rt/t/api/action-createtickets.t @@ -0,0 +1,240 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 49; + + +{ + +ok (require RT::Action::CreateTickets); +use_ok('RT::Scrip'); +use_ok('RT::Template'); +use_ok('RT::ScripAction'); +use_ok('RT::ScripCondition'); +use_ok('RT::Ticket'); + +my $approvalsq = RT::Queue->new($RT::SystemUser); +$approvalsq->Create(Name => 'Approvals'); +ok ($approvalsq->Id, "Created Approvals test queue"); + + +my $approvals = +'===Create-Ticket: approval +Queue: Approvals +Type: approval +AdminCc: {join ("\nAdminCc: ",@admins) } +Depended-On-By: {$Tickets{"TOP"}->Id} +Refers-To: TOP +Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject} +Due: {time + 86400} +Content-Type: text/plain +Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject} +Blah +Blah +ENDOFCONTENT +===Create-Ticket: two +Subject: Manager approval. +Depended-On-By: approval +Queue: Approvals +Content-Type: text/plain +Content: +Your minion approved ticket {$Tickets{"TOP"}->Id}. you ok with that? +ENDOFCONTENT +'; + +like ($approvals , qr/Content/, "Read in the approvals template"); + +my $apptemp = RT::Template->new($RT::SystemUser); +$apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0"); + +ok ($apptemp->Id); + +my $q = RT::Queue->new($RT::SystemUser); +$q->Create(Name => 'WorkflowTest'); +ok ($q->Id, "Created workflow test queue"); + +my $scrip = RT::Scrip->new($RT::SystemUser); +my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction', + ScripAction => 'Create Tickets', + Template => 'Approvals', + Queue => $q->Id); +ok ($sval, $smsg); +ok ($scrip->Id, "Created the scrip"); +ok ($scrip->TemplateObj->Id, "Created the scrip template"); +ok ($scrip->ConditionObj->Id, "Created the scrip condition"); +ok ($scrip->ActionObj->Id, "Created the scrip action"); + +my $t = RT::Ticket->new($RT::SystemUser); +my($tid, $ttrans, $tmsg) = $t->Create(Subject => "Sample workflow test", + Owner => "root", + Queue => $q->Id); + +ok ($tid,$tmsg); + +my $deps = $t->DependsOn; +is ($deps->Count, 1, "The ticket we created depends on one other ticket"); +my $dependson= $deps->First->TargetObj; +ok ($dependson->Id, "It depends on a real ticket"); +unlike ($dependson->Subject, qr/{/, "The subject doesn't have braces in it. that means we're interpreting expressions"); +is ($t->ReferredToBy->Count,1, "It's only referred to by one other ticket"); +is ($t->ReferredToBy->First->BaseObj->Id,$t->DependsOn->First->TargetObj->Id, "The same ticket that depends on it refers to it."); +use RT::Action::CreateTickets; +my $action = RT::Action::CreateTickets->new( CurrentUser => $RT::SystemUser); + +# comma-delimited templates +my $commas = <<"EOF"; +id,Queue,Subject,Owner,Content +ticket1,General,"foo, bar",root,blah +ticket2,General,foo bar,root,blah +ticket3,General,foo' bar,root,blah'boo +ticket4,General,foo' bar,,blah'boo +EOF + + +# Comma delimited templates with missing data +my $sparse_commas = <<"EOF"; +id,Queue,Subject,Owner,Requestor +ticket14,General,,,bobby +ticket15,General,,,tommy +ticket16,General,,suzie,tommy +ticket17,General,Foo "bar" baz,suzie,tommy +ticket18,General,'Foo "bar" baz',suzie,tommy +ticket19,General,'Foo bar' baz,suzie,tommy +EOF + + +# tab-delimited templates +my $tabs = <<"EOF"; +id\tQueue\tSubject\tOwner\tContent +ticket10\tGeneral\t"foo' bar"\troot\tblah' +ticket11\tGeneral\tfoo, bar\troot\tblah +ticket12\tGeneral\tfoo' bar\troot\tblah'boo +ticket13\tGeneral\tfoo' bar\t\tblah'boo +EOF + +my %expected; + +$expected{ticket1} = <<EOF; +Queue: General +Subject: foo, bar +Owner: root +Content: blah +ENDOFCONTENT +EOF + +$expected{ticket2} = <<EOF; +Queue: General +Subject: foo bar +Owner: root +Content: blah +ENDOFCONTENT +EOF + +$expected{ticket3} = <<EOF; +Queue: General +Subject: foo' bar +Owner: root +Content: blah'boo +ENDOFCONTENT +EOF + +$expected{ticket4} = <<EOF; +Queue: General +Subject: foo' bar +Owner: +Content: blah'boo +ENDOFCONTENT +EOF + +$expected{ticket10} = <<EOF; +Queue: General +Subject: foo' bar +Owner: root +Content: blah' +ENDOFCONTENT +EOF + +$expected{ticket11} = <<EOF; +Queue: General +Subject: foo, bar +Owner: root +Content: blah +ENDOFCONTENT +EOF + +$expected{ticket12} = <<EOF; +Queue: General +Subject: foo' bar +Owner: root +Content: blah'boo +ENDOFCONTENT +EOF + +$expected{ticket13} = <<EOF; +Queue: General +Subject: foo' bar +Owner: +Content: blah'boo +ENDOFCONTENT +EOF + + +$expected{'ticket14'} = <<EOF; +Queue: General +Subject: +Owner: +Requestor: bobby +EOF +$expected{'ticket15'} = <<EOF; +Queue: General +Subject: +Owner: +Requestor: tommy +EOF +$expected{'ticket16'} = <<EOF; +Queue: General +Subject: +Owner: suzie +Requestor: tommy +EOF +$expected{'ticket17'} = <<EOF; +Queue: General +Subject: Foo "bar" baz +Owner: suzie +Requestor: tommy +EOF +$expected{'ticket18'} = <<EOF; +Queue: General +Subject: Foo "bar" baz +Owner: suzie +Requestor: tommy +EOF +$expected{'ticket19'} = <<EOF; +Queue: General +Subject: 'Foo bar' baz +Owner: suzie +Requestor: tommy +EOF + + + + +$action->Parse(Content =>$commas); +$action->Parse(Content =>$sparse_commas); +$action->Parse(Content => $tabs); + +my %got; +foreach (@{ $action->{'create_tickets'} }) { + $got{$_} = $action->{'templates'}->{$_}; +} + +foreach my $id ( sort keys %expected ) { + ok(exists($got{"create-$id"}), "template exists for $id"); + is($got{"create-$id"}, $expected{$id}, "template is correct for $id"); +} + + +} + +1; diff --git a/rt/t/api/attachment.t b/rt/t/api/attachment.t new file mode 100644 index 000000000..07c46bad0 --- /dev/null +++ b/rt/t/api/attachment.t @@ -0,0 +1,45 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 4; + + +{ + +ok (require RT::Attachment); + + +} + +{ + +my $test1 = "From: jesse"; +my @headers = RT::Attachment->_SplitHeaders($test1); +is ($#headers, 0, $test1 ); + +my $test2 = qq{From: jesse +To: bobby +Subject: foo +}; + +@headers = RT::Attachment->_SplitHeaders($test2); +is ($#headers, 2, "testing a bunch of singline multiple headers" ); + + +my $test3 = qq{From: jesse +To: bobby, + Suzie, + Sally, + Joey: bizzy, +Subject: foo +}; + +@headers = RT::Attachment->_SplitHeaders($test3); +is ($#headers, 2, "testing a bunch of singline multiple headers" ); + + + +} + +1; diff --git a/rt/t/api/attribute-tests.t b/rt/t/api/attribute-tests.t new file mode 100644 index 000000000..90c3ddb7e --- /dev/null +++ b/rt/t/api/attribute-tests.t @@ -0,0 +1,86 @@ +use strict; +use warnings; +use RT; +use RT::Test tests => 34; + + + +my $runid = rand(200); + +my $attribute = "squelch-$runid"; + +ok(require RT::Attributes); + +my $user = RT::User->new($RT::SystemUser); +ok (UNIVERSAL::isa($user, 'RT::User')); +my ($id,$msg) = $user->Create(Name => 'attrtest-'.$runid); +ok ($id, $msg); +ok($user->id, "Created a test user"); + +ok(1, $user->Attributes->BuildSelectQuery); +my $attr = $user->Attributes; +# XXX: Order by id as some tests depend on it +$attr->OrderByCols({ FIELD => 'id' }); + +ok(1, $attr->BuildSelectQuery); + + +ok (UNIVERSAL::isa($attr,'RT::Attributes'), 'got the attributes object'); + +($id, $msg) = $user->AddAttribute(Name => 'TestAttr', Content => 'The attribute has content'); +ok ($id, $msg); +is ($attr->Count,1, " One attr after adding a first one"); + +my $first_attr = $user->FirstAttribute('TestAttr'); +ok($first_attr, "got some sort of attribute"); +isa_ok($first_attr, 'RT::Attribute'); +is($first_attr->Content, 'The attribute has content', "got the right content back"); + +($id, $msg) = $attr->DeleteEntry(Name => $runid); +ok(!$id, "Deleted non-existant entry - $msg"); +is ($attr->Count,1, "1 attr after deleting an empty attr"); + +my @names = $attr->Names; +is ("@names", "TestAttr"); + + +($id, $msg) = $user->AddAttribute(Name => $runid, Content => "First"); +ok($id, $msg); + +my $runid_attr = $user->FirstAttribute($runid); +ok($runid_attr, "got some sort of attribute"); +isa_ok($runid_attr, 'RT::Attribute'); +is($runid_attr->Content, 'First', "got the right content back"); + +is ($attr->Count,2, " Two attrs after adding an attribute named $runid"); +($id, $msg) = $user->AddAttribute(Name => $runid, Content => "Second"); +ok($id, $msg); + +$runid_attr = $user->FirstAttribute($runid); +ok($runid_attr, "got some sort of attribute"); +isa_ok($runid_attr, 'RT::Attribute'); +is($runid_attr->Content, 'First', "got the first content back still"); + +is ($attr->Count,3, " Three attrs after adding a secondvalue to $runid"); +($id, $msg) = $attr->DeleteEntry(Name => $runid, Content => "First"); +ok($id, $msg); +is ($attr->Count,2); + +#$attr->_DoSearch(); +($id, $msg) = $attr->DeleteEntry(Name => $runid, Content => "Second"); +ok($id, $msg); +is ($attr->Count,1); + +#$attr->_DoSearch(); +ok(1, $attr->BuildSelectQuery); +($id, $msg) = $attr->DeleteEntry(Name => "moose"); +ok(!$id, "Deleted non-existant entry - $msg"); +is ($attr->Count,1); + +ok(1, $attr->BuildSelectQuery); +@names = $attr->Names; +is("@names", "TestAttr"); + + + +1; diff --git a/rt/t/api/attribute.t b/rt/t/api/attribute.t new file mode 100644 index 000000000..cb2626ad8 --- /dev/null +++ b/rt/t/api/attribute.t @@ -0,0 +1,42 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 7; + + +{ + +my $user = $RT::SystemUser; +my ($id, $msg) = $user->AddAttribute(Name => 'SavedSearch', Content => { Query => 'Foo'} ); +ok ($id, $msg); +my $attr = RT::Attribute->new($RT::SystemUser); +$attr->Load($id); +is($attr->Name , 'SavedSearch'); +$attr->SetSubValues( Format => 'baz'); + +my $format = $attr->SubValue('Format'); +is ($format , 'baz'); + +$attr->SetSubValues( Format => 'bar'); +$format = $attr->SubValue('Format'); +is ($format , 'bar'); + +$attr->DeleteAllSubValues(); +$format = $attr->SubValue('Format'); +is ($format, undef); + +$attr->SetSubValues(Format => 'This is a format'); + +my $attr2 = RT::Attribute->new($RT::SystemUser); +$attr2->Load($id); +is ($attr2->SubValue('Format'), 'This is a format'); +$attr2->Delete; +my $attr3 = RT::Attribute->new($RT::SystemUser); +($id) = $attr3->Load($id); +is ($id, 0); + + +} + +1; diff --git a/rt/t/api/cf.t b/rt/t/api/cf.t new file mode 100644 index 000000000..98114c93f --- /dev/null +++ b/rt/t/api/cf.t @@ -0,0 +1,224 @@ +#!/usr/bin/perl + +use strict; +use warnings FATAL => 'all'; + +use RT::Test tests => 139; + +# Before we get going, ditch all object_cfs; this will remove +# all custom fields systemwide; +my $object_cfs = RT::ObjectCustomFields->new($RT::SystemUser); +$object_cfs->UnLimit(); +while (my $ocf = $object_cfs->Next) { + $ocf->Delete(); +} + + +my $queue = RT::Queue->new( $RT::SystemUser ); +$queue->Create( Name => 'RecordCustomFields-'.$$ ); +ok ($queue->id, "Created the queue"); + +my $queue2 = RT::Queue->new( $RT::SystemUser ); +$queue2->Create( Name => 'RecordCustomFields2' ); + +my $ticket = RT::Ticket->new( $RT::SystemUser ); +$ticket->Create( + Queue => $queue->Id, + Requestor => 'root@localhost', + Subject => 'RecordCustomFields1', +); + +my $cfs = $ticket->CustomFields; +is( $cfs->Count, 0 ); + +# Check that record has no any CF values yet {{{ +my $cfvs = $ticket->CustomFieldValues; +is( $cfvs->Count, 0 ); +is( $ticket->FirstCustomFieldValue, undef ); + +my $local_cf1 = RT::CustomField->new( $RT::SystemUser ); +$local_cf1->Create( Name => 'RecordCustomFields1-'.$$, Type => 'SelectSingle', Queue => $queue->id ); +$local_cf1->AddValue( Name => 'RecordCustomFieldValues11' ); +$local_cf1->AddValue( Name => 'RecordCustomFieldValues12' ); + +my $local_cf2 = RT::CustomField->new( $RT::SystemUser ); +$local_cf2->Create( Name => 'RecordCustomFields2-'.$$, Type => 'SelectSingle', Queue => $queue->id ); +$local_cf2->AddValue( Name => 'RecordCustomFieldValues21' ); +$local_cf2->AddValue( Name => 'RecordCustomFieldValues22' ); + +my $global_cf3 = RT::CustomField->new( $RT::SystemUser ); +$global_cf3->Create( Name => 'RecordCustomFields3-'.$$, Type => 'SelectSingle', Queue => 0 ); +$global_cf3->AddValue( Name => 'RecordCustomFieldValues31' ); +$global_cf3->AddValue( Name => 'RecordCustomFieldValues32' ); + +my $local_cf4 = RT::CustomField->new( $RT::SystemUser ); +$local_cf4->Create( Name => 'RecordCustomFields4', Type => 'SelectSingle', Queue => $queue2->id ); +$local_cf4->AddValue( Name => 'RecordCustomFieldValues41' ); +$local_cf4->AddValue( Name => 'RecordCustomFieldValues42' ); + + +my @custom_fields = ($local_cf1, $local_cf2, $global_cf3); + + +$cfs = $ticket->CustomFields; +is( $cfs->Count, 3 ); + +# Check that record has no any CF values yet {{{ +$cfvs = $ticket->CustomFieldValues; +is( $cfvs->Count, 0 ); +is( $ticket->FirstCustomFieldValue, undef ); + +# CF with ID -1 shouldnt exist at all +$cfvs = $ticket->CustomFieldValues( -1 ); +is( $cfvs->Count, 0 ); +is( $ticket->FirstCustomFieldValue( -1 ), undef ); + +$cfvs = $ticket->CustomFieldValues( 'SomeUnexpedCustomFieldName' ); +is( $cfvs->Count, 0 ); +is( $ticket->FirstCustomFieldValue( 'SomeUnexpedCustomFieldName' ), undef ); + +for (@custom_fields) { + $cfvs = $ticket->CustomFieldValues( $_->id ); + is( $cfvs->Count, 0 ); + + $cfvs = $ticket->CustomFieldValues( $_->Name ); + is( $cfvs->Count, 0 ); + is( $ticket->FirstCustomFieldValue( $_->id ), undef ); + is( $ticket->FirstCustomFieldValue( $_->Name ), undef ); +} +# }}} + +# try to add field value with fields that do not exist {{{ +my ($status, $msg) = $ticket->AddCustomFieldValue( Field => -1 , Value => 'foo' ); +ok(!$status, "shouldn't add value" ); +($status, $msg) = $ticket->AddCustomFieldValue( Field => 'SomeUnexpedCustomFieldName' , Value => 'foo' ); +ok(!$status, "shouldn't add value" ); +# }}} + +# {{{ +SKIP: { + + skip "TODO: We want fields that are not allowed to set unexpected values", 10; + for (@custom_fields) { + ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_ , Value => 'SomeUnexpectedCFValue' ); + ok( !$status, 'value doesn\'t exist'); + + ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_->id , Value => 'SomeUnexpectedCFValue' ); + ok( !$status, 'value doesn\'t exist'); + + ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_->Name , Value => 'SomeUnexpectedCFValue' ); + ok( !$status, 'value doesn\'t exist'); + } + + # Let check that we did not add value to be sure + # using only FirstCustomFieldValue sub because + # we checked other variants allready + for (@custom_fields) { + is( $ticket->FirstCustomFieldValue( $_->id ), undef ); + } + +} +# Add some values to our custom fields +for (@custom_fields) { + # this should be tested elsewhere + $_->AddValue( Name => 'Foo' ); + $_->AddValue( Name => 'Bar' ); +} + +my $test_add_delete_cycle = sub { + my $cb = shift; + for (@custom_fields) { + ($status, $msg) = $ticket->AddCustomFieldValue( Field => $cb->($_) , Value => 'Foo' ); + ok( $status, "message: $msg"); + } + + # does it exist? + $cfvs = $ticket->CustomFieldValues; + is( $cfvs->Count, 3, "We found all three custom fields on our ticket" ); + for (@custom_fields) { + $cfvs = $ticket->CustomFieldValues( $_->id ); + is( $cfvs->Count, 1 , "we found one custom field when searching by id"); + + $cfvs = $ticket->CustomFieldValues( $_->Name ); + is( $cfvs->Count, 1 , " We found one custom field when searching by name for " . $_->Name); + is( $ticket->FirstCustomFieldValue( $_->id ), 'Foo' , "first value by id is foo"); + is( $ticket->FirstCustomFieldValue( $_->Name ), 'Foo' , "first value by name is foo"); + } + # because our CFs are SingleValue then new value addition should override + for (@custom_fields) { + ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_ , Value => 'Bar' ); + ok( $status, "message: $msg"); + } + $cfvs = $ticket->CustomFieldValues; + is( $cfvs->Count, 3 ); + for (@custom_fields) { + $cfvs = $ticket->CustomFieldValues( $_->id ); + is( $cfvs->Count, 1 ); + + $cfvs = $ticket->CustomFieldValues( $_->Name ); + is( $cfvs->Count, 1 ); + is( $ticket->FirstCustomFieldValue( $_->id ), 'Bar' ); + is( $ticket->FirstCustomFieldValue( $_->Name ), 'Bar' ); + } + # delete it + for (@custom_fields ) { + ($status, $msg) = $ticket->DeleteCustomFieldValue( Field => $_ , Value => 'Bar' ); + ok( $status, "Deleted a custom field value 'Bar' for field ".$_->id.": $msg"); + } + $cfvs = $ticket->CustomFieldValues; + is( $cfvs->Count, 0, "The ticket (".$ticket->id.") no longer has any custom field values" ); + for (@custom_fields) { + $cfvs = $ticket->CustomFieldValues( $_->id ); + is( $cfvs->Count, 0, $ticket->id." has no values for cf ".$_->id ); + + $cfvs = $ticket->CustomFieldValues( $_->Name ); + is( $cfvs->Count, 0 , $ticket->id." has no values for cf '".$_->Name. "'" ); + is( $ticket->FirstCustomFieldValue( $_->id ), undef , "There is no first custom field value when loading by id" ); + is( $ticket->FirstCustomFieldValue( $_->Name ), undef, "There is no first custom field value when loading by Name" ); + } +}; + +# lets test cycle via CF id +$test_add_delete_cycle->( sub { return $_[0]->id } ); +# lets test cycle via CF object reference +$test_add_delete_cycle->( sub { return $_[0] } ); + +$ticket->AddCustomFieldValue( Field => $local_cf2->id , Value => 'Baz' ); +$ticket->AddCustomFieldValue( Field => $global_cf3->id , Value => 'Baz' ); +# now if we ask for cf values on RecordCustomFields4 we should not get any +$cfvs = $ticket->CustomFieldValues( 'RecordCustomFields4' ); +is( $cfvs->Count, 0, "No custom field values for non-Queue cf" ); +is( $ticket->FirstCustomFieldValue( 'RecordCustomFields4' ), undef, "No first custom field value for non-Queue cf" ); + +{ + my $cfname = $global_cf3->Name; + ($status, $msg) = $global_cf3->SetDisabled(1); + ok($status, "Disabled CF named $cfname"); + + my $load = RT::CustomField->new( $RT::SystemUser ); + $load->LoadByName( Name => $cfname); + ok($load->Id, "Loaded CF named $cfname"); + is($load->Id, $global_cf3->Id, "Can load disabled CFs"); + + my $dup = RT::CustomField->new( $RT::SystemUser ); + $dup->Create( Name => $cfname, Type => 'SelectSingle', Queue => 0 ); + ok($dup->Id, "Created CF with duplicate name"); + + $load->LoadByName( Name => $cfname); + is($load->Id, $dup->Id, "Loading by name gets non-disabled first"); + + $dup->SetDisabled(1); + $global_cf3->SetDisabled(0); + + $load->LoadByName( Name => $cfname); + is($load->Id, $global_cf3->Id, "Loading by name gets non-disabled first, even with order swapped"); +} + +#SKIP: { +# skip "TODO: should we add CF values to objects via CF Name?", 48; +# names are not unique + # lets test cycle via CF Name +# $test_add_delete_cycle->( sub { return $_[0]->Name } ); +#} + + diff --git a/rt/t/api/cf_combo_casacade.t b/rt/t/api/cf_combo_casacade.t new file mode 100644 index 000000000..b37345a6a --- /dev/null +++ b/rt/t/api/cf_combo_casacade.t @@ -0,0 +1,46 @@ +#!/usr/bin/perl +use warnings; +use strict; + +use RT::Test tests => 11; + +sub fails { ok(!$_[0], "This should fail: $_[1]") } +sub works { ok($_[0], $_[1] || 'This works') } + +sub new (*) { + my $class = shift; + return $class->new($RT::SystemUser); +} + +my $q = new(RT::Queue); +works($q->Create(Name => "CF-Pattern-".$$)); + +my $cf = new(RT::CustomField); +my @cf_args = (Name => $q->Name, Type => 'Combobox', Queue => $q->id); + +works($cf->Create(@cf_args)); + +# Set some CFVs with Category markers + +my $t = new(RT::Ticket); +my ($id,undef,$msg) = $t->Create(Queue => $q->id, Subject => 'CF Test'); +works($id,$msg); + +sub add_works { + works( + $cf->AddValue(Name => $_[0], Description => $_[0], Category => $_[1]) + ); +}; + +add_works('value1', '1. Category A'); +add_works('value2'); +add_works('value3', '1.1. A-sub one'); +add_works('value4', '1.2. A-sub two'); +add_works('value5', ''); + +my $cfv = $cf->Values->First; +is($cfv->Category, '1. Category A'); +works($cfv->SetCategory('1. Category AAA')); +is($cfv->Category, '1. Category AAA'); + +1; diff --git a/rt/t/api/cf_external.t b/rt/t/api/cf_external.t new file mode 100644 index 000000000..076871240 --- /dev/null +++ b/rt/t/api/cf_external.t @@ -0,0 +1,56 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +use RT; +use RT::Test nodata => 1, tests => 11; + +sub new (*) { + my $class = shift; + return $class->new($RT::SystemUser); +} + +use constant VALUES_CLASS => 'RT::CustomFieldValues::Groups'; + +my $q = new( RT::Queue ); +isa_ok( $q, 'RT::Queue' ); +my ($qid) = $q->Create( Name => "CF-External-". $$ ); +ok( $qid, "created queue" ); +my %arg = ( Name => $q->Name, + Type => 'Select', + Queue => $q->id, + MaxValues => 1, + ValuesClass => VALUES_CLASS ); + +my $cf = new( RT::CustomField ); +isa_ok( $cf, 'RT::CustomField' ); + +{ + my ($cfid) = $cf->Create( %arg ); + ok( $cfid, "created cf" ); + is( $cf->ValuesClass, VALUES_CLASS, "right values class" ); + ok( $cf->IsExternalValues, "custom field has external values" ); +} + +{ + # create at least on group for the tests + my $group = RT::Group->new( $RT::SystemUser ); + my ($ret, $msg) = $group->CreateUserDefinedGroup( Name => $q->Name ); + ok $ret, 'created group' or diag "error: $msg"; +} + +{ + my $values = $cf->Values; + isa_ok( $values, VALUES_CLASS ); + ok( $values->Count, "we have values" ); + my ($failure, $count) = (0, 0); + while( my $value = $values->Next ) { + $count++; + $failure = 1 unless $value->Name; + } + ok( !$failure, "all values have name" ); + is( $values->Count, $count, "count is correct" ); +} + +exit(0); diff --git a/rt/t/api/cf_pattern.t b/rt/t/api/cf_pattern.t new file mode 100644 index 000000000..89db2fea5 --- /dev/null +++ b/rt/t/api/cf_pattern.t @@ -0,0 +1,53 @@ +#!/usr/bin/perl +use warnings; +use strict; + +use RT; +use RT::Test tests => 17; + + +sub fails { ok(!$_[0], "This should fail: $_[1]") } +sub works { ok($_[0], $_[1] || 'This works') } + +sub new (*) { + my $class = shift; + return $class->new($RT::SystemUser); +} + +my $q = new(RT::Queue); +works($q->Create(Name => "CF-Pattern-".$$)); + +my $cf = new(RT::CustomField); +my @cf_args = (Name => $q->Name, Type => 'Freeform', Queue => $q->id, MaxValues => 1); + +fails($cf->Create(@cf_args, Pattern => ')))bad!regex(((')); +works($cf->Create(@cf_args, Pattern => 'good regex')); + +my $t = new(RT::Ticket); +my ($id,undef,$msg) = $t->Create(Queue => $q->id, Subject => 'CF Test'); +works($id,$msg); + +# OK, I'm thoroughly brain washed by HOP at this point now... +sub cnt { $t->CustomFieldValues($cf->id)->Count }; +sub add { $t->AddCustomFieldValue(Field => $cf->id, Value => $_[0]) }; +sub del { $t->DeleteCustomFieldValue(Field => $cf->id, Value => $_[0]) }; + +is(cnt(), 0, "No values yet"); +fails(add('not going to match')); +is(cnt(), 0, "No values yet"); +works(add('here is a good regex')); +is(cnt(), 1, "Value filled"); +fails(del('here is a good regex')); +is(cnt(), 1, "Single CF - Value _not_ deleted"); + +$cf->SetMaxValues(0); # Unlimited MaxValues + +works(del('here is a good regex')); +is(cnt(), 0, "Multiple CF - Value deleted"); + +fails($cf->SetPattern('(?{ "insert evil code here" })')); +works($cf->SetPattern('(?!)')); # reject everything +fails(add('')); +fails(add('...')); + +1; diff --git a/rt/t/api/cf_single_values.t b/rt/t/api/cf_single_values.t new file mode 100644 index 000000000..8e96edd44 --- /dev/null +++ b/rt/t/api/cf_single_values.t @@ -0,0 +1,38 @@ +#!/usr/bin/perl +use warnings; +use strict; + +use RT; +use RT::Test tests => 8; + + + +my $q = RT::Queue->new($RT::SystemUser); +my ($id,$msg) =$q->Create(Name => "CF-Single-".$$); +ok($id,$msg); + +my $cf = RT::CustomField->new($RT::SystemUser); +($id,$msg) = $cf->Create(Name => 'Single-'.$$, Type => 'Select', MaxValues => '1', Queue => $q->id); +ok($id,$msg); + + +($id,$msg) =$cf->AddValue(Name => 'First'); +ok($id,$msg); + +($id,$msg) =$cf->AddValue(Name => 'Second'); +ok($id,$msg); + + +my $t = RT::Ticket->new($RT::SystemUser); +($id,undef,$msg) = $t->Create(Queue => $q->id, + Subject => 'CF Test'); + +ok($id,$msg); +is($t->CustomFieldValues($cf->id)->Count, 0, "No values yet"); +$t->AddCustomFieldValue(Field => $cf->id, Value => 'First'); +is($t->CustomFieldValues($cf->id)->Count, 1, "One now"); + +$t->AddCustomFieldValue(Field => $cf->id, Value => 'Second'); +is($t->CustomFieldValues($cf->id)->Count, 1, "Still one"); + +1; diff --git a/rt/t/api/cf_transaction.t b/rt/t/api/cf_transaction.t new file mode 100644 index 000000000..1ed2ab932 --- /dev/null +++ b/rt/t/api/cf_transaction.t @@ -0,0 +1,60 @@ +#!/usr/bin/perl + +use warnings; +use strict; +use Data::Dumper; + +use RT::Test tests => 14; +use_ok('RT'); +use_ok('RT::Transactions'); + + +my $q = RT::Queue->new($RT::SystemUser); +my ($id,$msg) = $q->Create( Name => 'TxnCFTest'.$$); +ok($id,$msg); + +my $cf = RT::CustomField->new($RT::SystemUser); +($id,$msg) = $cf->Create(Name => 'Txnfreeform-'.$$, Type => 'Freeform', MaxValues => '0', LookupType => RT::Transaction->CustomFieldLookupType ); + +ok($id,$msg); + +($id,$msg) = $cf->AddToObject($q); + +ok($id,$msg); + + +my $ticket = RT::Ticket->new($RT::SystemUser); + +my $transid; +($id,$transid, $msg) = $ticket->Create(Queue => $q->id, + Subject => 'TxnCF test', + ); +ok($id,$msg); + +my $trans = RT::Transaction->new($RT::SystemUser); +$trans->Load($transid); + +is($trans->ObjectId,$id); +is ($trans->ObjectType, 'RT::Ticket'); +is ($trans->Type, 'Create'); +my $txncfs = $trans->CustomFields; +is ($txncfs->Count, 1, "We have one custom field"); +my $txn_cf = $txncfs->First; +is ($txn_cf->id, $cf->id, "It's the right custom field"); +my $values = $trans->CustomFieldValues($txn_cf->id); +is ($values->Count, 0, "It has no values"); + +# Old API +my %cf_updates = ( 'CustomField-'.$cf->id => 'Testing'); +$trans->UpdateCustomFields( ARGSRef => \%cf_updates); + + $values = $trans->CustomFieldValues($txn_cf->id); +is ($values->Count, 1, "It has one value"); + +# New API + +$trans->UpdateCustomFields( 'CustomField-'.$cf->id => 'Test two'); + $values = $trans->CustomFieldValues($txn_cf->id); +is ($values->Count, 2, "it has two values"); + +# TODO ok(0, "Should updating custom field values remove old values?"); diff --git a/rt/t/api/condition-ownerchange.t b/rt/t/api/condition-ownerchange.t new file mode 100644 index 000000000..4c4c49b29 --- /dev/null +++ b/rt/t/api/condition-ownerchange.t @@ -0,0 +1,51 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 11; + + +{ + +my $q = RT::Queue->new($RT::SystemUser); +$q->Create(Name =>'ownerChangeTest'); + +ok($q->Id, "Created a scriptest queue"); + +my $s1 = RT::Scrip->new($RT::SystemUser); +my ($val, $msg) =$s1->Create( Queue => $q->Id, + ScripAction => 'User Defined', + ScripCondition => 'On Owner Change', + CustomIsApplicableCode => '', + CustomPrepareCode => 'return 1', + CustomCommitCode => ' + $self->TicketObj->SetPriority($self->TicketObj->Priority+1); + return(1); + ', + Template => 'Blank' + ); +ok($val,$msg); + +my $ticket = RT::Ticket->new($RT::SystemUser); +my ($tv,$ttv,$tm) = $ticket->Create(Queue => $q->Id, + Subject => "hair on fire", + InitialPriority => '20' + ); +ok($tv, $tm); +ok($ticket->SetOwner('root')); +is ($ticket->Priority , '21', "Ticket priority is set right"); +ok($ticket->Steal); +is ($ticket->Priority , '22', "Ticket priority is set right"); +ok($ticket->Untake); +is ($ticket->Priority , '23', "Ticket priority is set right"); +ok($ticket->Take); +is ($ticket->Priority , '24', "Ticket priority is set right"); + + + + + + +} + +1; diff --git a/rt/t/api/condition-reject.t b/rt/t/api/condition-reject.t new file mode 100644 index 000000000..96789509d --- /dev/null +++ b/rt/t/api/condition-reject.t @@ -0,0 +1,45 @@ +# +# Check that the "On Reject" scrip condition exists and is working +# + +use strict; +use warnings; +use RT; +use RT::Test tests => 7; + + +{ + +my $q = RT::Queue->new($RT::SystemUser); +$q->Create(Name =>'rejectTest'); + +ok($q->Id, "Created a scriptest queue"); + +my $s1 = RT::Scrip->new($RT::SystemUser); +my ($val, $msg) =$s1->Create( Queue => $q->Id, + ScripAction => 'User Defined', + ScripCondition => 'On reject', + CustomIsApplicableCode => '', + CustomPrepareCode => 'return 1', + CustomCommitCode => ' + $self->TicketObj->SetPriority($self->TicketObj->Priority+1); + return(1); + ', + Template => 'Blank' + ); +ok($val,$msg); + +my $ticket = RT::Ticket->new($RT::SystemUser); +my ($tv,$ttv,$tm) = $ticket->Create(Queue => $q->Id, + Subject => "hair on fire", + InitialPriority => '20' + ); +ok($tv, $tm); +ok($ticket->SetStatus('rejected'), "Status set to \"rejected\""); +is ($ticket->Priority , '21', "Condition is true, scrip triggered"); +ok($ticket->SetStatus('open'), "Status set to \"open\""); +is ($ticket->Priority , '21', "Condition is false, scrip skipped"); + +} + +1; diff --git a/rt/t/api/currentuser.t b/rt/t/api/currentuser.t new file mode 100644 index 000000000..c15804824 --- /dev/null +++ b/rt/t/api/currentuser.t @@ -0,0 +1,32 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 8; + + +{ + +ok (require RT::CurrentUser); + + +} + +{ + +ok (my $cu = RT::CurrentUser->new('root')); +ok (my $lh = $cu->LanguageHandle('en-us')); +isnt ($lh, undef, '$lh is defined'); +ok ($lh->isa('Locale::Maketext')); +is ($cu->loc('TEST_STRING'), "Concrete Mixer", "Localized TEST_STRING into English"); +SKIP: { + skip "French localization is not enabled", 2 + unless grep $_ && $_ =~ /^(\*|fr)$/, RT->Config->Get('LexiconLanguages'); + ok ($lh = $cu->LanguageHandle('fr')); + is ($cu->loc('before'), "avant", "Localized TEST_STRING into French"); +} + + +} + +1; diff --git a/rt/t/api/customfield.t b/rt/t/api/customfield.t new file mode 100644 index 000000000..44319c47f --- /dev/null +++ b/rt/t/api/customfield.t @@ -0,0 +1,74 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 29; +use Test::Warn; + + +{ + +use_ok('RT::CustomField'); +ok(my $cf = RT::CustomField->new($RT::SystemUser)); +ok(my ($id, $msg)= $cf->Create( Name => 'TestingCF', + Queue => '0', + SortOrder => '1', + Description => 'A Testing custom field', + Type=> 'SelectSingle'), 'Created a global CustomField'); +isnt($id , 0, 'Global custom field correctly created'); +ok ($cf->SingleValue); +is($cf->Type, 'Select'); +is($cf->MaxValues, 1); + +(my $val, $msg) = $cf->SetMaxValues('0'); +ok($val, $msg); +is($cf->Type, 'Select'); +is($cf->MaxValues, 0); +ok(!$cf->SingleValue ); +ok(my ($bogus_val, $bogus_msg) = $cf->SetType('BogusType') , "Trying to set a custom field's type to a bogus type"); +is($bogus_val , 0, "Unable to set a custom field's type to a bogus type"); + +ok(my $bad_cf = RT::CustomField->new($RT::SystemUser)); +ok(my ($bad_id, $bad_msg)= $cf->Create( Name => 'TestingCF-bad', + Queue => '0', + SortOrder => '1', + Description => 'A Testing custom field with a bogus Type', + Type=> 'SelectSingleton'), 'Created a global CustomField with a bogus type'); +is($bad_id , 0, 'Global custom field correctly decided to not create a cf with a bogus type '); + + +} + +{ + +ok(my $cf = RT::CustomField->new($RT::SystemUser)); +$cf->Load(1); +is($cf->Id , 1); +ok(my ($val,$msg) = $cf->AddValue(Name => 'foo' , Description => 'TestCFValue', SortOrder => '6')); +isnt($val , 0); +ok (my ($delval, $delmsg) = $cf->DeleteValue($val)); +ok ($delval,"Deleting a cf value: $delmsg"); + + +} + +{ + +ok(my $cf = RT::CustomField->new($RT::SystemUser)); + +warning_like { +ok($cf->ValidateType('SelectSingle')); +} qr/deprecated/; + +warning_like { +ok($cf->ValidateType('SelectMultiple')); +} qr/deprecated/; + +warning_like { +ok(!$cf->ValidateType('SelectFooMultiple')); +} qr/deprecated/; + + +} + +1; diff --git a/rt/t/api/date.t b/rt/t/api/date.t new file mode 100644 index 000000000..bc1446f50 --- /dev/null +++ b/rt/t/api/date.t @@ -0,0 +1,564 @@ +#!/usr/bin/perl + +use Test::MockTime qw(set_fixed_time restore_time); + +use Test::More; +my $tests; + +my $localized_datetime_tests; +BEGIN { + $tests = 167; + $localized_datetime_tests = + eval { require DateTime; 1; } && eval { require DateTime::Locale; 1; } && + DateTime->can('format_cldr') && DateTime::Locale::root->can('date_format_full'); + + if ($localized_datetime_tests) { + + # Include RT::Date::LocalizedDateTime tests + $tests += 7; + } +} + +use warnings; use strict; +use RT::Test tests => $tests; +use RT::User; +use Test::Warn; + +use_ok('RT::Date'); +{ + my $date = RT::Date->new($RT::SystemUser); + isa_ok($date, 'RT::Date', "constructor returned RT::Date oject"); + $date = $date->new($RT::SystemUser); + isa_ok($date, 'RT::Date', "constructor returned RT::Date oject"); +} + +{ + # set timezone in all places to UTC + $RT::SystemUser->UserObj->__Set(Field => 'Timezone', Value => 'UTC') + if $RT::SystemUser->UserObj->Timezone; + RT->Config->Set( Timezone => 'UTC' ); +} + +my $current_user; +{ + my $user = RT::User->new($RT::SystemUser); + my($uid, $msg) = $user->Create( + Name => "date_api". rand(200), + Lang => 'en', + Privileged => 1, + ); + ok($uid, "user was created") or diag("error: $msg"); + $current_user = RT::CurrentUser->new($user); +} + +{ + my $date = RT::Date->new( $current_user ); + is($date->Timezone, 'UTC', "dropped all timzones to UTC"); + is($date->Timezone('user'), 'UTC', "dropped all timzones to UTC"); + is($date->Timezone('server'), 'UTC', "dropped all timzones to UTC"); + is($date->Timezone('unknown'), 'UTC', "with wrong context returns UTC"); + + $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow'); + is($current_user->UserObj->Timezone, + 'Europe/Moscow', + "successfuly changed user's timezone"); + is($date->Timezone('user'), + 'Europe/Moscow', + "in user context returns user's timezone"); + is($date->Timezone, 'UTC', "the deafult value is always UTC"); + is($date->Timezone('server'), 'UTC', "wasn't changed"); + + RT->Config->Set( Timezone => 'Africa/Ouagadougou' ); + is($date->Timezone('server'), + 'Africa/Ouagadougou', + "timezone of the RT server was changed"); + is($date->Timezone('user'), + 'Europe/Moscow', + "in user context still returns user's timezone"); + is($date->Timezone, 'UTC', "the deafult value is always UTC"); + + $current_user->UserObj->__Set( Field => 'Timezone', Value => ''); + is_empty($current_user->UserObj->Timezone, + "successfuly changed user's timezone"); + is($date->Timezone('user'), + 'Africa/Ouagadougou', + "in user context returns timezone of the server if user's one is not defined"); + is($date->Timezone, 'UTC', "the deafult value is always UTC"); + + RT->Config->Set( Timezone => 'GMT' ); + is($date->Timezone('server'), + 'UTC', + "timezone is GMT which one is alias for UTC"); + + RT->Config->Set( Timezone => '' ); + is($date->Timezone, 'UTC', "dropped all timzones to UTC"); + is($date->Timezone('user'), + 'UTC', + "user's and server's timzones are not defined, so UTC"); + is($date->Timezone('server'), + 'UTC', + "timezone of the server is not defined so UTC"); + + RT->Config->Set( Timezone => 'UTC' ); +} + +{ + my $date = RT::Date->new($RT::SystemUser); + is($date->Unix, 0, "new date returns 0 in Unix format"); + is($date->Get, '1970-01-01 00:00:00', "default is ISO format"); + is($date->Get(Format =>'SomeBadFormat'), + '1970-01-01 00:00:00', + "don't know format, return ISO format"); + is($date->Get(Format =>'W3CDTF'), + '1970-01-01T00:00:00Z', + "W3CDTF format with defaults"); + is($date->Get(Format =>'RFC2822'), + 'Thu, 1 Jan 1970 00:00:00 +0000', + "RFC2822 format with defaults"); + is($date->Get(Format =>'LocalizedDateTime'), + 'Thu, Jan 1, 1970 12:00:00 AM', + "LocalizedDateTime format with defaults") if ( $localized_datetime_tests ); + + is($date->ISO(Time => 0), + '1970-01-01', + "ISO format without time part"); + is($date->W3CDTF(Time => 0), + '1970-01-01', + "W3CDTF format without time part"); + is($date->RFC2822(Time => 0), + 'Thu, 1 Jan 1970', + "RFC2822 format without time part"); + is($date->LocalizedDateTime(Time => 0), + 'Thu, Jan 1, 1970', + "LocalizedDateTime format without time part") if ( $localized_datetime_tests ); + + is($date->ISO(Date => 0), + '00:00:00', + "ISO format without date part"); + is($date->W3CDTF(Date => 0), + '1970-01-01T00:00:00Z', + "W3CDTF format is incorrect without date part"); + is($date->RFC2822(Date => 0), + '00:00:00 +0000', + "RFC2822 format without date part"); + is($date->LocalizedDateTime(Date => 0), + '12:00:00 AM', + "LocalizedDateTime format without date part") if ( $localized_datetime_tests ); + + is($date->ISO(Date => 0, Seconds => 0), + '00:00', + "ISO format without date part and seconds"); + is($date->W3CDTF(Date => 0, Seconds => 0), + '1970-01-01T00:00Z', + "W3CDTF format without seconds, but we ship date part even if Date is false"); + is($date->RFC2822(Date => 0, Seconds => 0), + '00:00 +0000', + "RFC2822 format without date part and seconds"); + + is($date->RFC2822(DayOfWeek => 0), + '1 Jan 1970 00:00:00 +0000', + "RFC2822 format without 'day of week' part"); + is($date->RFC2822(DayOfWeek => 0, Date => 0), + '00:00:00 +0000', + "RFC2822 format without 'day of week' and date parts(corner case test)"); + + is($date->LocalizedDateTime(AbbrDay => 0), + 'Thursday, Jan 1, 1970 12:00:00 AM', + "LocalizedDateTime format without abbreviation of day") if ( $localized_datetime_tests ); + is($date->LocalizedDateTime(AbbrMonth => 0), + 'Thu, January 1, 1970 12:00:00 AM', + "LocalizedDateTime format without abbreviation of month") if ( $localized_datetime_tests ); + is($date->LocalizedDateTime(DateFormat => 'date_format_short'), + '1/1/70 12:00:00 AM', + "LocalizedDateTime format with non default DateFormat") if ( $localized_datetime_tests ); + is($date->LocalizedDateTime(TimeFormat => 'time_format_short'), + 'Thu, Jan 1, 1970 12:00 AM', + "LocalizedDateTime format with non default TimeFormat") if ( $localized_datetime_tests ); + + is($date->Date, + '1970-01-01', + "the default format for the 'Date' method is ISO"); + is($date->Date(Format => 'W3CDTF'), + '1970-01-01', + "'Date' method, W3CDTF format"); + is($date->Date(Format => 'RFC2822'), + 'Thu, 1 Jan 1970', + "'Date' method, RFC2822 format"); + is($date->Date(Time => 1), + '1970-01-01', + "'Date' method doesn't pass through 'Time' argument"); + is($date->Date(Date => 0), + '1970-01-01', + "'Date' method overrides 'Date' argument"); + + is($date->Time, + '00:00:00', + "the default format for the 'Time' method is ISO"); + is($date->Time(Format => 'W3CDTF'), + '1970-01-01T00:00:00Z', + "'Time' method, W3CDTF format, date part is required by w3c doc"); + is($date->Time(Format => 'RFC2822'), + '00:00:00 +0000', + "'Time' method, RFC2822 format"); + is($date->Time(Date => 1), + '00:00:00', + "'Time' method doesn't pass through 'Date' argument"); + is($date->Time(Time => 0), + '00:00:00', + "'Time' method overrides 'Time' argument"); + + is($date->DateTime, + '1970-01-01 00:00:00', + "the default format for the 'DateTime' method is ISO"); + is($date->DateTime(Format =>'W3CDTF'), + '1970-01-01T00:00:00Z', + "'DateTime' method, W3CDTF format"); + is($date->DateTime(Format =>'RFC2822'), + 'Thu, 1 Jan 1970 00:00:00 +0000', + "'DateTime' method, RFC2822 format"); + is($date->DateTime(Date => 0, Time => 0), + '1970-01-01 00:00:00', + "the 'DateTime' method overrides both 'Date' and 'Time' arguments"); +} + + +{ # positive timezone + $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow'); + my $date = RT::Date->new( $current_user ); + $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-01-01 15:10:00' ); + is($date->ISO( Timezone => 'user' ), '2005-01-01 18:10:00', "ISO"); + is($date->W3CDTF( Timezone => 'user' ), '2005-01-01T18:10:00+03:00', "W3C DTF"); + is($date->RFC2822( Timezone => 'user' ), 'Sat, 1 Jan 2005 18:10:00 +0300', "RFC2822"); + + # DST + $date = RT::Date->new( $current_user ); + $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-07-01 15:10:00' ); + is($date->ISO( Timezone => 'user' ), '2005-07-01 19:10:00', "ISO"); + is($date->W3CDTF( Timezone => 'user' ), '2005-07-01T19:10:00+04:00', "W3C DTF"); + is($date->RFC2822( Timezone => 'user' ), 'Fri, 1 Jul 2005 19:10:00 +0400', "RFC2822"); +} + +{ # negative timezone + $current_user->UserObj->__Set( Field => 'Timezone', Value => 'America/New_York'); + my $date = RT::Date->new( $current_user ); + $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-01-01 15:10:00' ); + is($date->ISO( Timezone => 'user' ), '2005-01-01 10:10:00', "ISO"); + is($date->W3CDTF( Timezone => 'user' ), '2005-01-01T10:10:00-05:00', "W3C DTF"); + is($date->RFC2822( Timezone => 'user' ), 'Sat, 1 Jan 2005 10:10:00 -0500', "RFC2822"); + + # DST + $date = RT::Date->new( $current_user ); + $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-07-01 15:10:00' ); + is($date->ISO( Timezone => 'user' ), '2005-07-01 11:10:00', "ISO"); + is($date->W3CDTF( Timezone => 'user' ), '2005-07-01T11:10:00-04:00', "W3C DTF"); + is($date->RFC2822( Timezone => 'user' ), 'Fri, 1 Jul 2005 11:10:00 -0400', "RFC2822"); +} + +warning_like +{ # bad format + my $date = RT::Date->new($RT::SystemUser); + $date->Set( Format => 'bad' ); + is($date->Unix, 0, "bad format"); +} qr'Unknown Date format: bad'; + + +{ # setting value via Unix method + my $date = RT::Date->new($RT::SystemUser); + $date->Unix(1); + is($date->ISO, '1970-01-01 00:00:01', "correct value"); + + foreach (undef, 0, ''){ + $date->Unix(1); + is($date->ISO, '1970-01-01 00:00:01', "correct value"); + + $date->Set(Format => 'unix', Value => $_); + is($date->ISO, '1970-01-01 00:00:00', "Set a date to midnight 1/1/1970 GMT due to wrong call"); + is($date->Unix, 0, "unix is 0 => unset"); + } +} + +my $year = (localtime(time))[5] + 1900; + +{ # set+ISO format + my $date = RT::Date->new($RT::SystemUser); + warning_like { + $date->Set(Format => 'ISO', Value => 'weird date'); + } qr/Couldn't parse date 'weird date' as a ISO format/; + is($date->Unix, 0, "date was wrong => unix == 0"); + + # XXX: ISO format has more feature than we suport + # http://www.cl.cam.ac.uk/~mgk25/iso-time.html + + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss"); + + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00+00'); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss+00"); + + $date->Set(Format => 'ISO', Value => '11-28 15:10:00'); + is($date->ISO, $year .'-11-28 15:10:00', "DD-MM hh:mm:ss"); + + $date->Set(Format => 'ISO', Value => '11-28 15:10:00+00'); + is($date->ISO, $year .'-11-28 15:10:00', "DD-MM hh:mm:ss+00"); + + $date->Set(Format => 'ISO', Value => '20051128151000'); + is($date->ISO, '2005-11-28 15:10:00', "YYYYDDMMhhmmss"); + + $date->Set(Format => 'ISO', Value => '1128151000'); + is($date->ISO, $year .'-11-28 15:10:00', "DDMMhhmmss"); + + $date->Set(Format => 'ISO', Value => '2005112815:10:00'); + is($date->ISO, '2005-11-28 15:10:00', "YYYYDDMMhh:mm:ss"); + + $date->Set(Format => 'ISO', Value => '112815:10:00'); + is($date->ISO, $year .'-11-28 15:10:00', "DDMMhh:mm:ss"); + + $date->Set(Format => 'ISO', Value => '2005-13-28 15:10:00'); + is($date->Unix, 0, "wrong month value"); + + $date->Set(Format => 'ISO', Value => '2005-00-28 15:10:00'); + is($date->Unix, 0, "wrong month value"); + + $date->Set(Format => 'ISO', Value => '1960-01-28 15:10:00'); + is($date->Unix, 0, "too old, we don't support"); +} + +{ # set+datemanip format(Time::ParseDate) + my $date = RT::Date->new($RT::SystemUser); + $date->Set(Format => 'unknown', Value => 'weird date'); + is($date->Unix, 0, "date was wrong"); + + RT->Config->Set( Timezone => 'Europe/Moscow' ); + $date->Set(Format => 'datemanip', Value => '2005-11-28 15:10:00'); + is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss"); + + RT->Config->Set( Timezone => 'UTC' ); + $date->Set(Format => 'datemanip', Value => '2005-11-28 15:10:00'); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss"); + + $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow'); + $date = RT::Date->new( $current_user ); + $date->Set(Format => 'datemanip', Value => '2005-11-28 15:10:00'); + is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss"); +} + +{ # set+unknown format(Time::ParseDate) + my $date = RT::Date->new($RT::SystemUser); + $date->Set(Format => 'unknown', Value => 'weird date'); + is($date->Unix, 0, "date was wrong"); + + RT->Config->Set( Timezone => 'Europe/Moscow' ); + $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00'); + is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss"); + + $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00', Timezone => 'utc' ); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss"); + + # test relative dates + { + set_fixed_time("2005-11-28T15:10:00Z"); + $date->Set(Format => 'unknown', Value => 'now'); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss"); + + $date->Set(Format => 'unknown', Value => '1 day ago'); + is($date->ISO, '2005-11-27 15:10:00', "YYYY-DD-MM hh:mm:ss"); + restore_time(); + } + + RT->Config->Set( Timezone => 'UTC' ); + $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00'); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss"); + + $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow'); + $date = RT::Date->new( $current_user ); + $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00'); + is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss"); + $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00', Timezone => 'server' ); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss"); + $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00', Timezone => 'utc' ); + is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss"); +} + +{ # SetToMidnight + my $date = RT::Date->new($RT::SystemUser); + + RT->Config->Set( Timezone => 'Europe/Moscow' ); + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + $date->SetToMidnight; + is($date->ISO, '2005-11-28 00:00:00', "default is utc"); + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + $date->SetToMidnight(Timezone => 'utc'); + is($date->ISO, '2005-11-28 00:00:00', "utc context"); + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + $date->SetToMidnight(Timezone => 'user'); + is($date->ISO, '2005-11-27 21:00:00', "user context, user has no preference, fallback to server"); + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + $date->SetToMidnight(Timezone => 'server'); + is($date->ISO, '2005-11-27 21:00:00', "server context"); + + $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow'); + $date = RT::Date->new( $current_user ); + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + $date->SetToMidnight; + is($date->ISO, '2005-11-28 00:00:00', "default is utc"); + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + $date->SetToMidnight(Timezone => 'utc'); + is($date->ISO, '2005-11-28 00:00:00', "utc context"); + $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00'); + $date->SetToMidnight(Timezone => 'user'); + is($date->ISO, '2005-11-27 21:00:00', "user context"); + $date->SetToMidnight(Timezone => 'server'); + is($date->ISO, '2005-11-27 21:00:00', "server context"); + + RT->Config->Set( Timezone => 'UTC' ); +} + +{ # SetToNow + my $date = RT::Date->new($RT::SystemUser); + my $time = time; + $date->SetToNow; + ok($date->Unix >= $time, 'close enough'); + ok($date->Unix < $time+5, 'difference is less than five seconds'); +} + +{ + my $date = RT::Date->new($RT::SystemUser); + + $date->Unix(0); + $date->AddSeconds; + is($date->ISO, '1970-01-01 00:00:00', "nothing changed"); + $date->AddSeconds(0); + is($date->ISO, '1970-01-01 00:00:00', "nothing changed"); + + $date->Unix(0); + $date->AddSeconds(5); + is($date->ISO, '1970-01-01 00:00:05', "added five seconds"); + $date->AddSeconds(-2); + is($date->ISO, '1970-01-01 00:00:03', "substracted two seconds"); + + $date->Unix(0); + $date->AddSeconds(3661); + is($date->ISO, '1970-01-01 01:01:01', "added one hour, minute and a second"); + +# XXX: TODO, doesn't work with Test::Warn +# TODO: { +# local $TODO = "BUG or subject to change Date handling to support unix time <= 0"; +# $date->Unix(0); +# $date->AddSeconds(-2); +# ok($date->Unix > 0); +# } + + $date->Unix(0); + $date->AddDay; + is($date->ISO, '1970-01-02 00:00:00', "added one day"); + $date->AddDays(2); + is($date->ISO, '1970-01-04 00:00:00', "added two days"); + $date->AddDays(-1); + is($date->ISO, '1970-01-03 00:00:00', "substructed one day"); + + $date->Unix(0); + $date->AddDays(31); + is($date->ISO, '1970-02-01 00:00:00', "added one month"); +} + +{ + $current_user->UserObj->__Set( Field => 'Timezone', Value => ''); + my $date = RT::Date->new( $current_user ); + is($date->AsString, "Not set", "AsString returns 'Not set'"); + + RT->Config->Set( DateTimeFormat => ''); + $date->Unix(1); + is($date->AsString, 'Thu Jan 01 00:00:01 1970', "correct string"); + is($date->AsString(Date => 0), '00:00:01', "correct string"); + is($date->AsString(Time => 0), 'Thu Jan 01 1970', "correct string"); + is($date->AsString(Date => 0, Time => 0), 'Thu Jan 01 00:00:01 1970', "invalid input"); + + RT->Config->Set( DateTimeFormat => 'RFC2822' ); + $date->Unix(1); + is($date->AsString, 'Thu, 1 Jan 1970 00:00:01 +0000', "correct string"); + + RT->Config->Set( DateTimeFormat => { Format => 'RFC2822', Seconds => 0 } ); + $date->Unix(1); + is($date->AsString, 'Thu, 1 Jan 1970 00:00 +0000', "correct string"); + is($date->AsString(Seconds => 1), 'Thu, 1 Jan 1970 00:00:01 +0000', "correct string"); +} + +{ # DurationAsString + my $date = RT::Date->new($RT::SystemUser); + + is($date->DurationAsString(1), '1 sec', '1 sec'); + is($date->DurationAsString(59), '59 sec', '59 sec'); + is($date->DurationAsString(60), '1 min', '1 min'); + is($date->DurationAsString(60*119), '119 min', '119 min'); + is($date->DurationAsString(60*60*2-1), '120 min', '120 min'); + is($date->DurationAsString(60*60*2), '2 hours', '2 hours'); + is($date->DurationAsString(60*60*48-1), '48 hours', '48 hours'); + is($date->DurationAsString(60*60*48), '2 days', '2 days'); + is($date->DurationAsString(60*60*24*14-1), '14 days', '14 days'); + is($date->DurationAsString(60*60*24*14), '2 weeks', '2 weeks'); + is($date->DurationAsString(60*60*24*7*8-1), '8 weeks', '8 weeks'); + is($date->DurationAsString(60*60*24*61), '2 months', '2 months'); + is($date->DurationAsString(60*60*24*365-1), '12 months', '12 months'); + is($date->DurationAsString(60*60*24*366), '1 years', '1 years'); + + is($date->DurationAsString(-1), '1 sec ago', '1 sec ago'); +} + +{ # DiffAsString + my $date = RT::Date->new($RT::SystemUser); + is($date->DiffAsString(1), '', 'no diff, wrong input'); + is($date->DiffAsString(-1), '', 'no diff, wrong input'); + is($date->DiffAsString('qwe'), '', 'no diff, wrong input'); + + $date->Unix(2); + is($date->DiffAsString(-1), '', 'no diff, wrong input'); + + is($date->DiffAsString(3), '1 sec ago', 'diff: 1 sec ago'); + is($date->DiffAsString(1), '1 sec', 'diff: 1 sec'); + + my $ndate = RT::Date->new($RT::SystemUser); + is($date->DiffAsString($ndate), '', 'no diff, wrong input'); + $ndate->Unix(3); + is($date->DiffAsString($ndate), '1 sec ago', 'diff: 1 sec ago'); +} + +{ # Diff + my $date = RT::Date->new($RT::SystemUser); + $date->SetToNow; + my $diff = $date->Diff; + ok($diff <= 0, 'close enought'); + ok($diff > -5, 'close enought'); +} + +{ # AgeAsString + my $date = RT::Date->new($RT::SystemUser); + $date->SetToNow; + my $diff = $date->AgeAsString; + like($diff, qr/^(0 sec|[1-5] sec ago)$/, 'close enought'); +} + +{ # GetWeekday + my $date = RT::Date->new($RT::SystemUser); + is($date->GetWeekday(7), '', '7 and greater are invalid'); + is($date->GetWeekday(6), 'Sat', '6 is Saturday'); + is($date->GetWeekday(0), 'Sun', '0 is Sunday'); + is($date->GetWeekday(-1), 'Sat', '-1 is Saturday'); + is($date->GetWeekday(-7), 'Sun', '-7 is Sunday'); + is($date->GetWeekday(-8), '', '-8 and lesser are invalid'); +} + +{ # GetMonth + my $date = RT::Date->new($RT::SystemUser); + is($date->GetMonth(12), '', '12 and greater are invalid'); + is($date->GetMonth(11), 'Dec', '11 is December'); + is($date->GetMonth(0), 'Jan', '0 is January'); + is($date->GetMonth(-1), 'Dec', '11 is December'); + is($date->GetMonth(-12), 'Jan', '0 is January'); + is($date->GetMonth(-13), '', '-13 and lesser are invalid'); +} + +#TODO: AsString +#TODO: RFC2822, W3CDTF with Timezones + +exit(0); + diff --git a/rt/t/api/emailparser.t b/rt/t/api/emailparser.t new file mode 100644 index 000000000..4807138cc --- /dev/null +++ b/rt/t/api/emailparser.t @@ -0,0 +1,32 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 4; + + +{ + +ok(require RT::EmailParser); + + +} + +{ + +is(RT::EmailParser::IsRTAddress("","rt\@example.com"),1, "Regexp matched rt address" ); +is(RT::EmailParser::IsRTAddress("","frt\@example.com"),undef, "Regexp didn't match non-rt address" ); + + +} + +{ + +my @before = ("rt\@example.com", "frt\@example.com"); +my @after = ("frt\@example.com"); +ok(eq_array(RT::EmailParser::CullRTAddresses("",@before),@after), "CullRTAddresses only culls RT addresses"); + + +} + +1; diff --git a/rt/t/api/group.t b/rt/t/api/group.t new file mode 100644 index 000000000..551d4f1a0 --- /dev/null +++ b/rt/t/api/group.t @@ -0,0 +1,99 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 38; + + +{ + +# {{{ Tests +ok (require RT::Group); + +ok (my $group = RT::Group->new($RT::SystemUser), "instantiated a group object"); +ok (my ($id, $msg) = $group->CreateUserDefinedGroup( Name => 'TestGroup', Description => 'A test group', + ), 'Created a new group'); +isnt ($id , 0, "Group id is $id"); +is ($group->Name , 'TestGroup', "The group's name is 'TestGroup'"); +my $ng = RT::Group->new($RT::SystemUser); + +ok($ng->LoadUserDefinedGroup('TestGroup'), "Loaded testgroup"); +is($ng->id , $group->id, "Loaded the right group"); + + +ok (($id,$msg) = $ng->AddMember('1'), "Added a member to the group"); +ok($id, $msg); +ok (($id,$msg) = $ng->AddMember('2' ), "Added a member to the group"); +ok($id, $msg); +ok (($id,$msg) = $ng->AddMember('3' ), "Added a member to the group"); +ok($id, $msg); + +# Group 1 now has members 1, 2 ,3 + +my $group_2 = RT::Group->new($RT::SystemUser); +ok (my ($id_2, $msg_2) = $group_2->CreateUserDefinedGroup( Name => 'TestGroup2', Description => 'A second test group'), , 'Created a new group'); +isnt ($id_2 , 0, "Created group 2 ok- $msg_2 "); +ok (($id,$msg) = $group_2->AddMember($ng->PrincipalId), "Made TestGroup a member of testgroup2"); +ok($id, $msg); +ok (($id,$msg) = $group_2->AddMember('1' ), "Added member RT_System to the group TestGroup2"); +ok($id, $msg); + +# Group 2 how has 1, g1->{1, 2,3} + +my $group_3 = RT::Group->new($RT::SystemUser); +ok (my ($id_3, $msg_3) = $group_3->CreateUserDefinedGroup( Name => 'TestGroup3', Description => 'A second test group'), 'Created a new group'); +isnt ($id_3 , 0, "Created group 3 ok - $msg_3"); +ok (($id,$msg) =$group_3->AddMember($group_2->PrincipalId), "Made TestGroup a member of testgroup2"); +ok($id, $msg); + +# g3 now has g2->{1, g1->{1,2,3}} + +my $principal_1 = RT::Principal->new($RT::SystemUser); +$principal_1->Load('1'); + +my $principal_2 = RT::Principal->new($RT::SystemUser); +$principal_2->Load('2'); + +ok (($id,$msg) = $group_3->AddMember('1' ), "Added member RT_System to the group TestGroup2"); +ok($id, $msg); + +# g3 now has 1, g2->{1, g1->{1,2,3}} + +is($group_3->HasMember($principal_2), undef, "group 3 doesn't have member 2"); +ok($group_3->HasMemberRecursively($principal_2), "group 3 has member 2 recursively"); +ok($ng->HasMember($principal_2) , "group ".$ng->Id." has member 2"); +my ($delid , $delmsg) =$ng->DeleteMember($principal_2->Id); +isnt ($delid ,0, "Sucessfully deleted it-".$delid."-".$delmsg); + +#Gotta reload the group objects, since we've been messing with various internals. +# we shouldn't need to do this. +#$ng->LoadUserDefinedGroup('TestGroup'); +#$group_2->LoadUserDefinedGroup('TestGroup2'); +#$group_3->LoadUserDefinedGroup('TestGroup'); + +# G1 now has 1, 3 +# Group 2 how has 1, g1->{1, 3} +# g3 now has 1, g2->{1, g1->{1, 3}} + +ok(!$ng->HasMember($principal_2) , "group ".$ng->Id." no longer has member 2"); +is($group_3->HasMemberRecursively($principal_2), undef, "group 3 doesn't have member 2"); +is($group_2->HasMemberRecursively($principal_2), undef, "group 2 doesn't have member 2"); +is($ng->HasMember($principal_2), undef, "group 1 doesn't have member 2"); +is($group_3->HasMemberRecursively($principal_2), undef, "group 3 has member 2 recursively"); + +# }}} + + +} + +{ + +ok(my $u = RT::Group->new($RT::SystemUser)); +ok($u->Load(4), "Loaded the first user"); +is($u->PrincipalObj->ObjectId , 4, "user 4 is the fourth principal"); +is($u->PrincipalObj->PrincipalType , 'Group' , "Principal 4 is a group"); + + +} + +1; diff --git a/rt/t/api/groups.t b/rt/t/api/groups.t new file mode 100644 index 000000000..995c844ba --- /dev/null +++ b/rt/t/api/groups.t @@ -0,0 +1,139 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 28; + + +{ + +ok (require RT::Groups); + + +} + +{ + +# next had bugs +# Groups->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => xx ); +my $g = RT::Group->new($RT::SystemUser); +my ($id, $msg) = $g->CreateUserDefinedGroup(Name => 'GroupsNotEqualTest'); +ok ($id, "created group #". $g->id) or diag("error: $msg"); + +my $groups = RT::Groups->new($RT::SystemUser); +$groups->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $g->id ); +$groups->LimitToUserDefinedGroups(); +my $bug = grep $_->id == $g->id, @{$groups->ItemsArrayRef}; +ok (!$bug, "didn't find group"); + + +} + +{ + +my $u = RT::User->new($RT::SystemUser); +my ($id, $msg) = $u->Create( Name => 'Membertests'. $$ ); +ok ($id, 'created user') or diag "error: $msg"; + +my $g = RT::Group->new($RT::SystemUser); +($id, $msg) = $g->CreateUserDefinedGroup(Name => 'Membertests'); +ok ($id, $msg); + +my ($aid, $amsg) =$g->AddMember($u->id); +ok ($aid, $amsg); +ok($g->HasMember($u->PrincipalObj),"G has member u"); + +my $groups = RT::Groups->new($RT::SystemUser); +$groups->LimitToUserDefinedGroups(); +$groups->WithMember(PrincipalId => $u->id); +is ($groups->Count , 1,"found the 1 group - " . $groups->Count); +is ($groups->First->Id , $g->Id, "it's the right one"); + + +} + +{ + no warnings qw/redefine once/; + +my $q = RT::Queue->new($RT::SystemUser); +my ($id, $msg) =$q->Create( Name => 'GlobalACLTest'); +ok ($id, $msg); + +my $testuser = RT::User->new($RT::SystemUser); +($id,$msg) = $testuser->Create(Name => 'JustAnAdminCc'); +ok ($id,$msg); + +my $global_admin_cc = RT::Group->new($RT::SystemUser); +$global_admin_cc->LoadSystemRoleGroup('AdminCc'); +ok($global_admin_cc->id, "Found the global admincc group"); +my $groups = RT::Groups->new($RT::SystemUser); +$groups->WithRight(Right => 'OwnTicket', Object => $q); +is($groups->Count, 1); +($id, $msg) = $global_admin_cc->PrincipalObj->GrantRight(Right =>'OwnTicket', Object=> $RT::System); +ok ($id,$msg); +ok (!$testuser->HasRight(Object => $q, Right => 'OwnTicket') , "The test user does not have the right to own tickets in the test queue"); +($id, $msg) = $q->AddWatcher(Type => 'AdminCc', PrincipalId => $testuser->id); +ok($id,$msg); +ok ($testuser->HasRight(Object => $q, Right => 'OwnTicket') , "The test user does have the right to own tickets now. thank god."); + +$groups = RT::Groups->new($RT::SystemUser); +$groups->WithRight(Right => 'OwnTicket', Object => $q); +ok ($id,$msg); +is($groups->Count, 3); + +my $RTxGroup = RT::Group->new($RT::SystemUser); +($id, $msg) = $RTxGroup->CreateUserDefinedGroup( Name => 'RTxGroup', Description => "RTx extension group"); +ok ($id,$msg); +is ($RTxGroup->id, $id, "group loaded"); + +my $RTxSysObj = {}; +bless $RTxSysObj, 'RTx::System'; +*RTx::System::Id = sub { 1; }; +*RTx::System::id = *RTx::System::Id; +my $ace = RT::Record->new($RT::SystemUser); +$ace->Table('ACL'); +$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)}); +($id, $msg) = $ace->Create( PrincipalId => $RTxGroup->id, PrincipalType => 'Group', RightName => 'RTxGroupRight', ObjectType => 'RTx::System', ObjectId => 1); +ok ($id, "ACL for RTxSysObj created"); + +my $RTxObj = {}; +bless $RTxObj, 'RTx::System::Record'; +*RTx::System::Record::Id = sub { 4; }; +*RTx::System::Record::id = *RTx::System::Record::Id; + +$groups = RT::Groups->new($RT::SystemUser); +$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxSysObj); +is($groups->Count, 1, "RTxGroupRight found for RTxSysObj"); + +$groups = RT::Groups->new($RT::SystemUser); +$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj); +is($groups->Count, 0, "RTxGroupRight not found for RTxObj"); + +$groups = RT::Groups->new($RT::SystemUser); +$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj, EquivObjects => [ $RTxSysObj ]); +is($groups->Count, 1, "RTxGroupRight found for RTxObj using EquivObjects"); + +$ace = RT::Record->new($RT::SystemUser); +$ace->Table('ACL'); +$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)}); +($id, $msg) = $ace->Create( PrincipalId => $RTxGroup->id, PrincipalType => 'Group', RightName => 'RTxGroupRight', ObjectType => 'RTx::System::Record', ObjectId => 5 ); +ok ($id, "ACL for RTxObj created"); + +my $RTxObj2 = {}; +bless $RTxObj2, 'RTx::System::Record'; +*RTx::System::Record::Id = sub { 5; }; + +$groups = RT::Groups->new($RT::SystemUser); +$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj2); +is($groups->Count, 1, "RTxGroupRight found for RTxObj2"); + +$groups = RT::Groups->new($RT::SystemUser); +$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj2, EquivObjects => [ $RTxSysObj ]); +is($groups->Count, 1, "RTxGroupRight found for RTxObj2"); + + + + +} + +1; diff --git a/rt/t/api/i18n.t b/rt/t/api/i18n.t new file mode 100644 index 000000000..17d71b761 --- /dev/null +++ b/rt/t/api/i18n.t @@ -0,0 +1,30 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 9; + + +{ + +use_ok ('RT::I18N'); +ok(RT::I18N->Init); + + +} + +{ + +ok(my $chinese = RT::I18N->get_handle('zh_tw')); +ok(UNIVERSAL::can($chinese, 'maketext')); +like($chinese->maketext('__Content-Type') , qr/utf-8/i, "Found the utf-8 charset for traditional chinese in the string ".$chinese->maketext('__Content-Type')); +is($chinese->encoding , 'utf-8', "The encoding is 'utf-8' -".$chinese->encoding); + +ok(my $en = RT::I18N->get_handle('en')); +ok(UNIVERSAL::can($en, 'maketext')); +is($en->encoding , 'utf-8', "The encoding ".$en->encoding." is 'utf-8'"); + + +} + +1; diff --git a/rt/t/api/link.t b/rt/t/api/link.t new file mode 100644 index 000000000..1fd66bb64 --- /dev/null +++ b/rt/t/api/link.t @@ -0,0 +1,24 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 5; + + +{ + + +use RT::Link; +my $link = RT::Link->new($RT::SystemUser); + + +ok (ref $link); +ok (UNIVERSAL::isa($link, 'RT::Link')); +ok (UNIVERSAL::isa($link, 'RT::Base')); +ok (UNIVERSAL::isa($link, 'RT::Record')); +ok (UNIVERSAL::isa($link, 'DBIx::SearchBuilder::Record')); + + +} + +1; diff --git a/rt/t/api/queue.t b/rt/t/api/queue.t new file mode 100644 index 000000000..44d5cafce --- /dev/null +++ b/rt/t/api/queue.t @@ -0,0 +1,92 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 24; + + +{ + +use RT::Queue; + + +} + +{ + +my $q = RT::Queue->new($RT::SystemUser); +is($q->IsValidStatus('new'), 1, 'New is a valid status'); +is($q->IsValidStatus('f00'), 0, 'f00 is not a valid status'); + + +} + +{ + +my $q = RT::Queue->new($RT::SystemUser); +is($q->IsActiveStatus('new'), 1, 'New is a Active status'); +is($q->IsActiveStatus('rejected'), 0, 'Rejected is an inactive status'); +is($q->IsActiveStatus('f00'), 0, 'f00 is not a Active status'); + + +} + +{ + +my $q = RT::Queue->new($RT::SystemUser); +is($q->IsInactiveStatus('new'), 0, 'New is a Active status'); +is($q->IsInactiveStatus('rejected'), 1, 'rejeected is an Inactive status'); +is($q->IsInactiveStatus('f00'), 0, 'f00 is not a Active status'); + + +} + +{ + +my $queue = RT::Queue->new($RT::SystemUser); +my ($id, $val) = $queue->Create( Name => 'Test1'); +ok($id, $val); + +($id, $val) = $queue->Create( Name => '66'); +ok(!$id, $val); + + +} + +{ + +my $Queue = RT::Queue->new($RT::SystemUser); +my ($id, $msg) = $Queue->Create(Name => "Foo"); +ok ($id, "Foo $id was created"); +ok(my $group = RT::Group->new($RT::SystemUser)); +ok($group->LoadQueueRoleGroup(Queue => $id, Type=> 'Requestor')); +ok ($group->Id, "Found the requestors object for this Queue"); + +{ + my ($status, $msg) = $Queue->AddWatcher(Type => 'Cc', Email => 'bob@fsck.com'); + ok ($status, "Added bob at fsck.com as a requestor") or diag "error: $msg"; +} + +ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user"); +$bob->LoadByEmail('bob@fsck.com'); +ok($bob->Id, "Found the bob rt user"); +ok ($Queue->IsWatcher(Type => 'Cc', PrincipalId => $bob->PrincipalId), "The Queue actually has bob at fsck.com as a requestor"); + +{ + my ($status, $msg) = $Queue->DeleteWatcher(Type =>'Cc', Email => 'bob@fsck.com'); + ok ($status, "Deleted bob from Ccs") or diag "error: $msg"; + ok (!$Queue->IsWatcher(Type => 'Cc', PrincipalId => $bob->PrincipalId), + "The Queue no longer has bob at fsck.com as a requestor"); +} + +$group = RT::Group->new($RT::SystemUser); +ok($group->LoadQueueRoleGroup(Queue => $id, Type=> 'Cc')); +ok ($group->Id, "Found the cc object for this Queue"); +$group = RT::Group->new($RT::SystemUser); +ok($group->LoadQueueRoleGroup(Queue => $id, Type=> 'AdminCc')); +ok ($group->Id, "Found the AdminCc object for this Queue"); + + +} + +1; diff --git a/rt/t/api/record.t b/rt/t/api/record.t new file mode 100644 index 000000000..6bf1af81e --- /dev/null +++ b/rt/t/api/record.t @@ -0,0 +1,70 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 22; + + +{ + +ok (require RT::Record); + + +} + +{ + +my $ticket = RT::Ticket->new($RT::SystemUser); +my $group = RT::Group->new($RT::SystemUser); +is($ticket->ObjectTypeStr, 'Ticket', "Ticket returns correct typestring"); +is($group->ObjectTypeStr, 'Group', "Group returns correct typestring"); + + +} + +{ + +my $t1 = RT::Ticket->new($RT::SystemUser); +my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general'); +ok($id, "Created dep test 1 - $msg"); + +my $t2 = RT::Ticket->new($RT::SystemUser); +(my $id2, $trans, my $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general'); +ok($id2, "Created dep test 2 - $msg2"); +my $t3 = RT::Ticket->new($RT::SystemUser); +(my $id3, $trans, my $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval'); +ok($id3, "Created dep test 3 - $msg3"); +my ($addid, $addmsg); +ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t2->id)); +ok ($addid, $addmsg); +ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t3->id)); + +ok ($addid, $addmsg); +my $link = RT::Link->new($RT::SystemUser); +(my $rv, $msg) = $link->Load($addid); +ok ($rv, $msg); +is ($link->LocalTarget , $t3->id, "Link LocalTarget is correct"); +is ($link->LocalBase , $t1->id, "Link LocalBase is correct"); + +ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps"); +ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs"); +ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals"); +ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps"); +; + +my ($rid, $rmsg)= $t1->Resolve(); +ok(!$rid, $rmsg); +my ($rid2, $rmsg2) = $t2->Resolve(); +ok ($rid2, $rmsg2); +($rid, $rmsg)= $t1->Resolve(); +ok(!$rid, $rmsg); +my ($rid3,$rmsg3) = $t3->Resolve; +ok ($rid3,$rmsg3); +($rid, $rmsg)= $t1->Resolve(); +ok($rid, $rmsg); + + + +} + +1; diff --git a/rt/t/api/reminders.t b/rt/t/api/reminders.t new file mode 100644 index 000000000..fd1c6a69f --- /dev/null +++ b/rt/t/api/reminders.t @@ -0,0 +1,88 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 20; + + +{ + +# Create test queues +use_ok ('RT::Queue'); + +ok(my $testqueue = RT::Queue->new($RT::SystemUser), 'Instantiate RT::Queue'); +ok($testqueue->Create( Name => 'reminders tests'), 'Create new queue: reminders tests'); +isnt($testqueue->Id , 0, 'Success creating queue'); + +ok($testqueue->Create( Name => 'reminders tests 2'), 'Create new queue: reminders tests 2'); +isnt($testqueue->Id , 0, 'Success creating queue'); + +# Create test ticket +use_ok('RT::Ticket'); + +my $u = RT::User->new($RT::SystemUser); +$u->Load("root"); +ok ($u->Id, "Found the root user"); +ok(my $t = RT::Ticket->new($RT::SystemUser), 'Instantiate RT::Ticket'); +ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id, + Subject => 'Testing', + Owner => $u->Id + ), 'Create sample ticket'); +isnt($id , 0, 'Success creating ticket'); + +# Add reminder +my $due_obj = RT::Date->new( $RT::SystemUser ); +$due_obj->SetToNow; +ok(my ( $add_id, $add_msg, $txnid ) = $t->Reminders->Add( + Subject => 'TestReminder', + Owner => 'root', + Due => $due_obj->ISO + ), 'Add reminder'); + +# Check that the new Reminder is here +my $reminders = $t->Reminders->Collection; +ok($reminders, 'Loading reminders for this ticket'); +my $found = 0; +while ( my $reminder = $reminders->Next ) { + next unless $found == 0; + $found = 1 if ( $reminder->Subject =~ m/TestReminder/ ); +} + +is($found, 1, 'Reminder successfully added'); + +# Change queue +ok (my ($move_val, $move_msg) = $t->SetQueue('reminders tests 2'), 'Moving ticket from queue "reminders tests" to "reminders tests 2"'); + +is ($t->QueueObj->Name, 'reminders tests 2', 'Ticket successfully moved'); + +# Check that the new reminder is still there and moved to the new queue +$reminders = $t->Reminders->Collection; +ok($reminders, 'Loading reminders for this ticket'); +$found = 0; +my $ok_queue = 0; +while ( my $reminder = $reminders->Next ) { + next unless $found == 0; + if ( $reminder->Subject =~ m/TestReminder/ ) { + $found = 1; + $ok_queue = 1 if ( $reminder->QueueObj->Name eq 'reminders tests 2' ); + } +} +is($found, 1, 'Reminder successfully added'); + +is($ok_queue, 1, 'Reminder automatically moved to new queue'); + +# Resolve reminder +my $r_resolved = 0; +while ( my $reminder = $reminders->Next ) { + if ( $reminder->Subject =~ m/TestReminder/ ) { + if ( $reminder->Status ne 'resolved' ) { + $t->Reminders->Resolve($reminder); + $r_resolved = 1 if ( $reminder->Status eq 'resolved' ); + } + } +} + +is($r_resolved, 1, 'Reminder resolved'); + +} +1; diff --git a/rt/t/api/rights.t b/rt/t/api/rights.t new file mode 100644 index 000000000..7bd332f13 --- /dev/null +++ b/rt/t/api/rights.t @@ -0,0 +1,142 @@ +#!/usr/bin/perl -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC +# <jesse.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/copyleft/gpl.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use RT; +use RT::Test tests => 26; + +use RT::I18N; +use strict; +no warnings 'once'; + +use RT::Queue; +use RT::ACE; +use RT::User; +use RT::Group; +use RT::Ticket; + + +# clear all global right +my $acl = RT::ACL->new($RT::SystemUser); +$acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' ); +$acl->LimitToObject( $RT::System ); +while( my $ace = $acl->Next ) { + $ace->Delete; +} + +my $rand_name = "rights". int rand($$); +# create new queue to be shure we don't mess with rights +my $queue = RT::Queue->new($RT::SystemUser); +my ($queue_id) = $queue->Create( Name => $rand_name); +ok( $queue_id, 'queue created for rights tests' ); + +# new privileged user to check rights +my $user = RT::User->new( $RT::SystemUser ); +my ($user_id) = $user->Create( Name => $rand_name, + EmailAddress => $rand_name .'@localhost', + Privileged => 1, + Password => 'qwe123', + ); +ok( !$user->HasRight( Right => 'OwnTicket', Object => $queue ), "user can't own ticket" ); +ok( !$user->HasRight( Right => 'ReplyToTicket', Object => $queue ), "user can't reply to ticket" ); + +my $group = RT::Group->new( $RT::SystemUser ); +ok( $group->LoadQueueRoleGroup( Queue => $queue_id, Type=> 'Owner' ), "load queue owners role group" ); +my $ace = RT::ACE->new( $RT::SystemUser ); +my ($ace_id, $msg) = $group->PrincipalObj->GrantRight( Right => 'ReplyToTicket', Object => $queue ); +ok( $ace_id, "Granted queue owners role group with ReplyToTicket right: $msg" ); +ok( $group->PrincipalObj->HasRight( Right => 'ReplyToTicket', Object => $queue ), "role group can reply to ticket" ); +ok( !$user->HasRight( Right => 'ReplyToTicket', Object => $queue ), "user can't reply to ticket" ); + +# new ticket +my $ticket = RT::Ticket->new($RT::SystemUser); +my ($ticket_id) = $ticket->Create( Queue => $queue_id, Subject => 'test'); +ok( $ticket_id, 'new ticket created' ); +is( $ticket->Owner, $RT::Nobody->Id, 'owner of the new ticket is nobody' ); + +my $status; +($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'OwnTicket' ); +ok( $status, "successfuly granted right: $msg" ); +ok( $user->HasRight( Right => 'OwnTicket', Object => $queue ), "user can own ticket" ); + +($status, $msg) = $ticket->SetOwner( $user_id ); +ok( $status, "successfuly set owner: $msg" ); +is( $ticket->Owner, $user_id, "set correct owner" ); + +ok( $user->HasRight( Right => 'ReplyToTicket', Object => $ticket ), "user is owner and can reply to ticket" ); + +# Testing of EquivObjects +$group = RT::Group->new( $RT::SystemUser ); +ok( $group->LoadQueueRoleGroup( Queue => $queue_id, Type=> 'AdminCc' ), "load queue AdminCc role group" ); +$ace = RT::ACE->new( $RT::SystemUser ); +($ace_id, $msg) = $group->PrincipalObj->GrantRight( Right => 'ModifyTicket', Object => $queue ); +ok( $ace_id, "Granted queue AdminCc role group with ModifyTicket right: $msg" ); +ok( $group->PrincipalObj->HasRight( Right => 'ModifyTicket', Object => $queue ), "role group can modify ticket" ); +ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket ), "user is not AdminCc and can't modify ticket" ); +($status, $msg) = $ticket->AddWatcher(Type => 'AdminCc', PrincipalId => $user->PrincipalId); +ok( $status, "successfuly added user as AdminCc"); +ok( $user->HasRight( Right => 'ModifyTicket', Object => $ticket ), "user is AdminCc and can modify ticket" ); + +my $ticket2 = RT::Ticket->new($RT::SystemUser); +my ($ticket2_id) = $ticket2->Create( Queue => $queue_id, Subject => 'test2'); +ok( $ticket2_id, 'new ticket created' ); +ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket2 ), "user is not AdminCc and can't modify ticket2" ); + +# now we can finally test EquivObjects +my $equiv = [ $ticket ]; +ok( $user->HasRight( Right => 'ModifyTicket', Object => $ticket2, EquivObjects => $equiv ), + "user is not AdminCc but can modify ticket2 because of EquivObjects" ); + +# the first a third test below are the same, so they should both pass +my $equiv2 = []; +ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket2, EquivObjects => $equiv2 ), + "user is not AdminCc and can't modify ticket2" ); +ok( $user->HasRight( Right => 'ModifyTicket', Object => $ticket, EquivObjects => $equiv2 ), + "user is AdminCc and can modify ticket" ); +ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket2, EquivObjects => $equiv2 ), + "user is not AdminCc and can't modify ticket2 (same question different answer)" ); diff --git a/rt/t/api/rt.t b/rt/t/api/rt.t new file mode 100644 index 000000000..3c06b5848 --- /dev/null +++ b/rt/t/api/rt.t @@ -0,0 +1,18 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 4; + + +{ + +is ($RT::Nobody->Name() , 'Nobody', "Nobody is nobody"); +isnt ($RT::Nobody->Name() , 'root', "Nobody isn't named root"); +is ($RT::SystemUser->Name() , 'RT_System', "The system user is RT_System"); +isnt ($RT::SystemUser->Name() , 'noname', "The system user isn't noname"); + + +} + +1; diff --git a/rt/t/api/scrip.t b/rt/t/api/scrip.t new file mode 100644 index 000000000..8e8f96213 --- /dev/null +++ b/rt/t/api/scrip.t @@ -0,0 +1,49 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 7; + + +{ + +ok (require RT::Scrip); + + +my $q = RT::Queue->new($RT::SystemUser); +$q->Create(Name => 'ScripTest'); +ok($q->Id, "Created a scriptest queue"); + +my $s1 = RT::Scrip->new($RT::SystemUser); +my ($val, $msg) =$s1->Create( Queue => $q->Id, + ScripAction => 'User Defined', + ScripCondition => 'User Defined', + CustomIsApplicableCode => 'if ($self->TicketObj->Subject =~ /fire/) { return (1);} else { return(0)}', + CustomPrepareCode => 'return 1', + CustomCommitCode => '$self->TicketObj->SetPriority("87");', + Template => 'Blank' + ); +ok($val,$msg); + +my $ticket = RT::Ticket->new($RT::SystemUser); +my ($tv,$ttv,$tm) = $ticket->Create(Queue => $q->Id, + Subject => "hair on fire", + ); +ok($tv, $tm); + +is ($ticket->Priority , '87', "Ticket priority is set right"); + + +my $ticket2 = RT::Ticket->new($RT::SystemUser); +my ($t2v,$t2tv,$t2m) = $ticket2->Create(Queue => $q->Id, + Subject => "hair in water", + ); +ok($t2v, $t2m); + +isnt ($ticket2->Priority , '87', "Ticket priority is set right"); + + + +} + +1; diff --git a/rt/t/api/scrip_order.t b/rt/t/api/scrip_order.t new file mode 100644 index 000000000..9738db9bc --- /dev/null +++ b/rt/t/api/scrip_order.t @@ -0,0 +1,56 @@ +#!/usr/bin/perl -w + +use strict; + +use RT; +use RT::Test tests => 7; + + +# {{{ test scrip ordering based on description + +my $scrip_queue = RT::Queue->new($RT::SystemUser); +my ($queue_id, $msg) = $scrip_queue->Create( Name => "ScripOrdering-$$", + Description => 'Test scrip ordering by description' ); +ok($queue_id, "Created scrip-ordering test queue? ".$msg); + +my $priority_ten_scrip = RT::Scrip->new($RT::SystemUser); +(my $id, $msg) = $priority_ten_scrip->Create( + Description => "10 set priority $$", + Queue => $queue_id, + ScripCondition => 'On Create', + ScripAction => 'User Defined', + CustomPrepareCode => '$RT::Logger->debug("Setting priority to 10..."); return 1;', + CustomCommitCode => '$self->TicketObj->SetPriority(10);', + Template => 'Blank', + Stage => 'TransactionCreate', +); +ok($id, "Created priority-10 scrip? ".$msg); + +my $priority_five_scrip = RT::Scrip->new($RT::SystemUser); +($id, $msg) = $priority_ten_scrip->Create( + Description => "05 set priority $$", + Queue => $queue_id, + ScripCondition => 'On Create', + ScripAction => 'User Defined', + CustomPrepareCode => '$RT::Logger->debug("Setting priority to 5..."); return 1;', + CustomCommitCode => '$self->TicketObj->SetPriority(5);', + Template => 'Blank', + Stage => 'TransactionCreate', +); +ok($id, "Created priority-5 scrip? ".$msg); + +my $ticket = RT::Ticket->new($RT::SystemUser); +($id, $msg) = $ticket->Create( + Queue => $queue_id, + Requestor => 'order@example.com', + Subject => "Scrip order test $$", +); +ok($ticket->id, "Created ticket? id=$id"); + +isnt($ticket->Priority , 0, "Ticket shouldn't be priority 0"); +isnt($ticket->Priority , 5, "Ticket shouldn't be priority 5"); +is ($ticket->Priority , 10, "Ticket should be priority 10"); + +# }}} + +1; diff --git a/rt/t/api/searchbuilder.t b/rt/t/api/searchbuilder.t new file mode 100644 index 000000000..cb118906c --- /dev/null +++ b/rt/t/api/searchbuilder.t @@ -0,0 +1,40 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 11; + + +{ + +ok (require RT::SearchBuilder); + + +} + +{ + +use_ok('RT::Queues'); +ok(my $queues = RT::Queues->new($RT::SystemUser), 'Created a queues object'); +ok( $queues->UnLimit(),'Unlimited the result set of the queues object'); +my $items = $queues->ItemsArrayRef(); +my @items = @{$items}; + +ok($queues->NewItem->_Accessible('Name','read')); +my @sorted = sort {lc($a->Name) cmp lc($b->Name)} @items; +ok (@sorted, "We have an array of queues, sorted". join(',',map {$_->Name} @sorted)); + +ok (@items, "We have an array of queues, raw". join(',',map {$_->Name} @items)); +my @sorted_ids = map {$_->id } @sorted; +my @items_ids = map {$_->id } @items; + +is ($#sorted, $#items); +is ($sorted[0]->Name, $items[0]->Name); +is ($sorted[-1]->Name, $items[-1]->Name); +is_deeply(\@items_ids, \@sorted_ids, "ItemsArrayRef sorts alphabetically by name"); + + + +} + +1; diff --git a/rt/t/api/system.t b/rt/t/api/system.t new file mode 100644 index 000000000..3077115c7 --- /dev/null +++ b/rt/t/api/system.t @@ -0,0 +1,33 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 7; + + +{ + +my $s = RT::System->new($RT::SystemUser); +my $rights = $s->AvailableRights; +ok ($rights, "Rights defined"); +ok ($rights->{'AdminUsers'},"AdminUsers right found"); +ok ($rights->{'CreateTicket'},"CreateTicket right found"); +ok ($rights->{'AdminGroupMembership'},"ModifyGroupMembers right found"); +ok (!$rights->{'CasdasdsreateTicket'},"bogus right not found"); + + + + +} + +{ + +use RT::System; +my $sys = RT::System->new(); +is( $sys->Id, 1); +is ($sys->id, 1); + + +} + +1; diff --git a/rt/t/api/template-insert.t b/rt/t/api/template-insert.t new file mode 100644 index 000000000..47bbd790c --- /dev/null +++ b/rt/t/api/template-insert.t @@ -0,0 +1,26 @@ +#!/usr/bin/perl + +use warnings; +use strict; + + +use RT; +use RT::Test tests => 7; + + + +# This tiny little test script triggers an interaction bug between DBD::Oracle 1.16, SB 1.15 and RT 3.4 + +use_ok('RT::Template'); +my $template = RT::Template->new($RT::SystemUser); + +isa_ok($template, 'RT::Template'); +my ($val,$msg) = $template->Create(Queue => 1, + Name => 'InsertTest', + Content => 'This is template content'); +ok($val,$msg); +is($template->Name, 'InsertTest'); +is($template->Content, 'This is template content', "We created the object right"); +($val, $msg) = $template->SetContent( 'This is new template content'); +ok($val,$msg); +is($template->Content, 'This is new template content', "We managed to _Set_ the content"); diff --git a/rt/t/api/template.t b/rt/t/api/template.t new file mode 100644 index 000000000..1612b8ffd --- /dev/null +++ b/rt/t/api/template.t @@ -0,0 +1,26 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 2; + + +{ + +ok(require RT::Template); + + +} + +{ + +my $t = RT::Template->new($RT::SystemUser); +$t->Create(Name => "Foo", Queue => 1); +my $t2 = RT::Template->new($RT::Nobody); +$t2->Load($t->Id); +ok($t2->QueueObj->id, "Got the template's queue objet"); + + +} + +1; diff --git a/rt/t/api/ticket.t b/rt/t/api/ticket.t new file mode 100644 index 000000000..2ca0997bd --- /dev/null +++ b/rt/t/api/ticket.t @@ -0,0 +1,257 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 87; + + +{ + +use_ok ('RT::Queue'); +ok(my $testqueue = RT::Queue->new($RT::SystemUser)); +ok($testqueue->Create( Name => 'ticket tests')); +isnt($testqueue->Id , 0); +use_ok('RT::CustomField'); +ok(my $testcf = RT::CustomField->new($RT::SystemUser)); +my ($ret, $cmsg) = $testcf->Create( Name => 'selectmulti', + Queue => $testqueue->id, + Type => 'SelectMultiple'); +ok($ret,"Created the custom field - ".$cmsg); +($ret,$cmsg) = $testcf->AddValue ( Name => 'Value1', + SortOrder => '1', + Description => 'A testing value'); + +ok($ret, "Added a value - ".$cmsg); + +ok($testcf->AddValue ( Name => 'Value2', + SortOrder => '2', + Description => 'Another testing value')); +ok($testcf->AddValue ( Name => 'Value3', + SortOrder => '3', + Description => 'Yet Another testing value')); + +is($testcf->Values->Count , 3); + +use_ok('RT::Ticket'); + +my $u = RT::User->new($RT::SystemUser); +$u->Load("root"); +ok ($u->Id, "Found the root user"); +ok(my $t = RT::Ticket->new($RT::SystemUser)); +ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id, + Subject => 'Testing', + Owner => $u->Id + )); +isnt($id , 0); +is ($t->OwnerObj->Id , $u->Id, "Root is the ticket owner"); +ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id, + Value => 'Value1')); +isnt($cfv , 0, "Custom field creation didn't return an error: $cfm"); +is($t->CustomFieldValues($testcf->Id)->Count , 1); +ok($t->CustomFieldValues($testcf->Id)->First && + $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1'); + +ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id, + Value => 'Value1')); +isnt ($cfdv , 0, "Deleted a custom field value: $cfdm"); +is($t->CustomFieldValues($testcf->Id)->Count , 0); + +ok(my $t2 = RT::Ticket->new($RT::SystemUser)); +ok($t2->Load($id)); +is($t2->Subject, 'Testing'); +is($t2->QueueObj->Id, $testqueue->id); +is($t2->OwnerObj->Id, $u->Id); + +my $t3 = RT::Ticket->new($RT::SystemUser); +my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id, + Subject => 'Testing', + Owner => $u->Id); +my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id, + Value => 'Value1'); +isnt($cfv1 , 0, "Adding a custom field to ticket 1 is successful: $cfm"); +my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id, + Value => 'Value2'); +isnt($cfv2 , 0, "Adding a custom field to ticket 2 is successful: $cfm"); +my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id, + Value => 'Value3'); +isnt($cfv3 , 0, "Adding a custom field to ticket 1 is successful: $cfm"); +is($t->CustomFieldValues($testcf->Id)->Count , 2, + "This ticket has 2 custom field values"); +is($t3->CustomFieldValues($testcf->Id)->Count , 1, + "This ticket has 1 custom field value"); + + +} + +{ + + +ok(require RT::Ticket, "Loading the RT::Ticket library"); + + +} + +{ + +my $t = RT::Ticket->new($RT::SystemUser); + +ok( $t->Create(Queue => 'General', Due => '2002-05-21 00:00:00', ReferredToBy => 'http://www.cpan.org', RefersTo => 'http://fsck.com', Subject => 'This is a subject'), "Ticket Created"); + +ok ( my $id = $t->Id, "Got ticket id"); +like ($t->RefersTo->First->Target , qr/fsck.com/, "Got refers to"); +like ($t->ReferredToBy->First->Base , qr/cpan.org/, "Got referredtoby"); +is ($t->ResolvedObj->Unix, 0, "It hasn't been resolved - ". $t->ResolvedObj->Unix); + + +} + +{ + +my $ticket = RT::Ticket->new($RT::SystemUser); +my ($id, $msg) = $ticket->Create(Subject => "Foo", + Owner => $RT::SystemUser->Id, + Status => 'open', + Requestor => ['jesse@example.com'], + Queue => '1' + ); +ok ($id, "Ticket $id was created"); +ok(my $group = RT::Group->new($RT::SystemUser)); +ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor')); +ok ($group->Id, "Found the requestors object for this ticket"); + +ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user"); +$jesse->LoadByEmail('jesse@example.com'); +ok($jesse->Id, "Found the jesse rt user"); + + +ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor"); +ok (my ($add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor"); +ok ($add_id, "Add succeeded: ($add_msg)"); +ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user"); +$bob->LoadByEmail('bob@fsck.com'); +ok($bob->Id, "Found the bob rt user"); +ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor"); +ok ( ($add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor"); +ok (!$ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor"); + + +$group = RT::Group->new($RT::SystemUser); +ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc')); +ok ($group->Id, "Found the cc object for this ticket"); +$group = RT::Group->new($RT::SystemUser); +ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc')); +ok ($group->Id, "Found the AdminCc object for this ticket"); +$group = RT::Group->new($RT::SystemUser); +ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner')); +ok ($group->Id, "Found the Owner object for this ticket"); +ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'"); + + +} + +{ + +my $t = RT::Ticket->new($RT::SystemUser); +ok($t->Create(Queue => 'general', Subject => 'SquelchTest', SquelchMailTo => 'nobody@example.com')); + +my @returned = $t->SquelchMailTo(); +is($#returned, 0, "The ticket has one squelched recipients"); + +my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com'); +ok($ret, "Removed nobody as a squelched recipient - ".$msg); +@returned = $t->SquelchMailTo(); +is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned)); + + + +@returned = $t->SquelchMailTo('nobody@example.com'); +is($#returned, 0, "The ticket has one squelched recipients"); + +my @names = $t->Attributes->Names; +is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo"); +@returned = $t->SquelchMailTo('nobody@example.com'); + + +is($#returned, 0, "The ticket has one squelched recipients"); + +@names = $t->Attributes->Names; +is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo"); + + +($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com'); +ok($ret, "Removed nobody as a squelched recipient - ".$msg); +@returned = $t->SquelchMailTo(); +is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned)); + + + +} + +{ + +my $t1 = RT::Ticket->new($RT::SystemUser); +$t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com'); +my $t1id = $t1->id; +my $t2 = RT::Ticket->new($RT::SystemUser); +$t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com'); +my $t2id = $t2->id; +my ($msg, $val) = $t1->MergeInto($t2->id); +ok ($msg,$val); +$t1 = RT::Ticket->new($RT::SystemUser); +is ($t1->id, undef, "ok. we've got a blank ticket1"); +$t1->Load($t1id); + +is ($t1->id, $t2->id); + +is ($t1->Requestors->MembersObj->Count, 2); + + + +} + +{ + +my $root = RT::User->new($RT::SystemUser); +$root->Load('root'); +ok ($root->Id, "Loaded the root user"); +my $t = RT::Ticket->new($RT::SystemUser); +$t->Load(1); +$t->SetOwner('root'); +is ($t->OwnerObj->Name, 'root' , "Root owns the ticket"); +$t->Steal(); +is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket"); +my $txns = RT::Transactions->new($RT::SystemUser); +$txns->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$txns->Limit(FIELD => 'ObjectId', VALUE => '1'); +$txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket'); +$txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord'); + +my $steal = $txns->First; +is($steal->OldValue , $root->Id , "Stolen from root"); +is($steal->NewValue , $RT::SystemUser->Id , "Stolen by the systemuser"); + + +} + +{ + +my $tt = RT::Ticket->new($RT::SystemUser); +my ($id, $tid, $msg)= $tt->Create(Queue => 'general', + Subject => 'test'); +ok($id, $msg); +is($tt->Status, 'new', "New ticket is created as new"); + +($id, $msg) = $tt->SetStatus('open'); +ok($id, $msg); +like($msg, qr/open/i, "Status message is correct"); +($id, $msg) = $tt->SetStatus('resolved'); +ok($id, $msg); +like($msg, qr/resolved/i, "Status message is correct"); +($id, $msg) = $tt->SetStatus('resolved'); +ok(!$id,$msg); + + + +} + +1; diff --git a/rt/t/api/tickets.t b/rt/t/api/tickets.t new file mode 100644 index 000000000..9148a8899 --- /dev/null +++ b/rt/t/api/tickets.t @@ -0,0 +1,104 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 16; + + +{ + +ok (require RT::Tickets); +ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) ); +ok( $testtickets->LimitStatus( VALUE => 'deleted' ) ); +# Should be zero until 'allow_deleted_search' +is( $testtickets->Count , 0 ); + + +} + +{ + +# Test to make sure that you can search for tickets by requestor address and +# by requestor name. + +my ($id,$msg); +my $u1 = RT::User->new($RT::SystemUser); +($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com'); +ok ($id,$msg); +my $u2 = RT::User->new($RT::SystemUser); +($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com'); +ok ($id,$msg); + +my $t1 = RT::Ticket->new($RT::SystemUser); +my ($trans); +($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]); +ok ($id, $msg); + +my $t2 = RT::Ticket->new($RT::SystemUser); +($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]); +ok ($id, $msg); + + +my $t3 = RT::Ticket->new($RT::SystemUser); +($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]); +ok ($id, $msg); + + +my $tix1 = RT::Tickets->new($RT::SystemUser); +$tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"'); + +is ($tix1->Count, 3); + +my $tix2 = RT::Tickets->new($RT::SystemUser); +$tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"'); + +is ($tix2->Count, 3); + + +my $tix3 = RT::Tickets->new($RT::SystemUser); +$tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"'); + +is ($tix3->Count, 2); + +my $tix4 = RT::Tickets->new($RT::SystemUser); +$tix4->FromSQL('Requestor.Name LIKE "TestOne" '); + +is ($tix4->Count, 2); + +# Searching for tickets that have two requestors isn't supported +# There's no way to differentiate "one requestor name that matches foo and bar" +# and "two requestors, one matching foo and one matching bar" + +# my $tix5 = RT::Tickets->new($RT::SystemUser); +# $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"'); +# +# is ($tix5->Count, 1); +# +# my $tix6 = RT::Tickets->new($RT::SystemUser); +# $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"'); +# +# is ($tix6->Count, 1); + + + +} + +{ + +my $t1 = RT::Ticket->new($RT::SystemUser); +$t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']); + + +} + +{ + +# We assume that we've got some tickets hanging around from before. +ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) ); +ok( $unlimittickets->UnLimit ); +ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" ); + + +} + +1; diff --git a/rt/t/api/tickets_overlay_sql.t b/rt/t/api/tickets_overlay_sql.t new file mode 100644 index 000000000..798088664 --- /dev/null +++ b/rt/t/api/tickets_overlay_sql.t @@ -0,0 +1,73 @@ + +use RT; +use RT::Test tests => 7; + + +{ + +use RT::Tickets; +use strict; + +my $tix = RT::Tickets->new($RT::SystemUser); +{ + my $query = "Status = 'open'"; + my ($status, $msg) = $tix->FromSQL($query); + ok ($status, "correct query") or diag("error: $msg"); +} + + +my (@created,%created); +my $string = 'subject/content SQL test'; +{ + my $t = RT::Ticket->new($RT::SystemUser); + ok( $t->Create(Queue => 'General', Subject => $string), "Ticket Created"); + $created{ $t->Id }++; push @created, $t->Id; +} + +{ + my $Message = MIME::Entity->build( + Subject => 'this is my subject', + From => 'jesse@example.com', + Data => [ $string ], + ); + + my $t = RT::Ticket->new($RT::SystemUser); + ok( $t->Create( Queue => 'General', + Subject => 'another ticket', + MIMEObj => $Message, + MemberOf => $created[0] + ), + "Ticket Created" + ); + $created{ $t->Id }++; push @created, $t->Id; +} + +{ + my $query = ("Subject LIKE '$string' OR Content LIKE '$string'"); + my ($status, $msg) = $tix->FromSQL($query); + ok ($status, "correct query") or diag("error: $msg"); + + my $count = 0; + while (my $tick = $tix->Next) { + $count++ if $created{ $tick->id }; + } + is ($count, scalar @created, "number of returned tickets same as entered"); +} + +{ + my $query = "id = $created[0] OR MemberOf = $created[0]"; + my ($status, $msg) = $tix->FromSQL($query); + ok ($status, "correct query") or diag("error: $msg"); + + my $count = 0; + while (my $tick = $tix->Next) { + $count++ if $created{ $tick->id }; + } + is ($count, scalar @created, "number of returned tickets same as entered"); +} + + + +} + +1; diff --git a/rt/t/api/uri-fsck_com_rt.t b/rt/t/api/uri-fsck_com_rt.t new file mode 100644 index 000000000..d62e58022 --- /dev/null +++ b/rt/t/api/uri-fsck_com_rt.t @@ -0,0 +1,28 @@ +use strict; +use warnings; +use RT; +use RT::Test tests => 8; + +use_ok("RT::URI::fsck_com_rt"); +my $uri = RT::URI::fsck_com_rt->new($RT::SystemUser); + +my $t1 = RT::Ticket->new($RT::SystemUser); +my ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', ); +ok ($id, $msg); + +ok(ref($uri)); + +ok (UNIVERSAL::isa($uri,"RT::URI::fsck_com_rt"), "It's an RT::URI::fsck_com_rt"); + +ok ($uri->isa('RT::URI::base'), "It's an RT::URI::base"); +ok ($uri->isa('RT::Base'), "It's an RT::Base"); + +is ($uri->LocalURIPrefix , 'fsck.com-rt://'.RT->Config->Get('Organization')); + + +my $ticket = RT::Ticket->new($RT::SystemUser); +$ticket->Load(1); +$uri = RT::URI::fsck_com_rt->new($ticket->CurrentUser); +is($uri->LocalURIPrefix. "/ticket/1" , $uri->URIForObject($ticket)); + +1; diff --git a/rt/t/api/uri-t.t b/rt/t/api/uri-t.t new file mode 100644 index 000000000..4695629bb --- /dev/null +++ b/rt/t/api/uri-t.t @@ -0,0 +1,21 @@ +use strict; +use warnings; +use RT; +use RT::Test tests => 6; + +my $t1 = RT::Ticket->new($RT::SystemUser); +my ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', ); +ok ($id, $msg); + +use_ok("RT::URI::t"); +my $uri = RT::URI::t->new($RT::SystemUser); +ok(ref($uri), "URI object exists"); + +my $uristr = "t:1"; +$uri->ParseURI($uristr); +is(ref($uri->Object), "RT::Ticket", "Object loaded is a ticket"); +is($uri->Object->Id, 1, "Object loaded has correct ID"); +is($uri->URI, 'fsck.com-rt://'.RT->Config->Get('Organization').'/ticket/1', + "URI object has correct URI string"); + +1; diff --git a/rt/t/api/user.t b/rt/t/api/user.t new file mode 100644 index 000000000..25cf74773 --- /dev/null +++ b/rt/t/api/user.t @@ -0,0 +1,339 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 108; + + +{ + +ok(require RT::User); + + +} + +{ + +# Make sure we can create a user + +my $u1 = RT::User->new($RT::SystemUser); +is(ref($u1), 'RT::User'); +my ($id, $msg) = $u1->Create(Name => 'CreateTest1'.$$, EmailAddress => $$.'create-test-1@example.com'); +ok ($id, "Creating user CreateTest1 - " . $msg ); + +# Make sure we can't create a second user with the same name +my $u2 = RT::User->new($RT::SystemUser); +($id, $msg) = $u2->Create(Name => 'CreateTest1'.$$, EmailAddress => $$.'create-test-2@example.com'); +ok (!$id, $msg); + + +# Make sure we can't create a second user with the same EmailAddress address +my $u3 = RT::User->new($RT::SystemUser); +($id, $msg) = $u3->Create(Name => 'CreateTest2'.$$, EmailAddress => $$.'create-test-1@example.com'); +ok (!$id, $msg); + +# Make sure we can create a user with no EmailAddress address +my $u4 = RT::User->new($RT::SystemUser); +($id, $msg) = $u4->Create(Name => 'CreateTest3'.$$); +ok ($id, $msg); + +# make sure we can create a second user with no EmailAddress address +my $u5 = RT::User->new($RT::SystemUser); +($id, $msg) = $u5->Create(Name => 'CreateTest4'.$$); +ok ($id, $msg); + +# make sure we can create a user with a blank EmailAddress address +my $u6 = RT::User->new($RT::SystemUser); +($id, $msg) = $u6->Create(Name => 'CreateTest6'.$$, EmailAddress => ''); +ok ($id, $msg); +# make sure we can create a second user with a blankEmailAddress address +my $u7 = RT::User->new($RT::SystemUser); +($id, $msg) = $u7->Create(Name => 'CreateTest7'.$$, EmailAddress => ''); +ok ($id, $msg); + +# Can we change the email address away from from ""; +($id,$msg) = $u7->SetEmailAddress('foo@bar'.$$); +ok ($id, $msg); +# can we change the address back to ""; +($id,$msg) = $u7->SetEmailAddress(''); +ok ($id, $msg); +is_empty ($u7->EmailAddress); + +RT->Config->Set('ValidateUserEmailAddresses' => 1); +# Make sur we can't create a user with multiple email adresses separated by comma +my $u8 = RT::User->new($RT::SystemUser); +($id, $msg) = $u8->Create(Name => 'CreateTest8'.$$, EmailAddress => $$.'create-test-81@example.com, '.$$.'create-test-82@example.com'); +ok (!$id, $msg); + +# Make sur we can't create a user with multiple email adresses separated by space +my $u9 = RT::User->new($RT::SystemUser); +($id, $msg) = $u9->Create(Name => 'CreateTest9'.$$, EmailAddress => $$.'create-test-91@example.com '.$$.'create-test-92@example.com'); +ok (!$id, $msg); + +# Make sur we can't create a user with invalid email address +my $u10 = RT::User->new($RT::SystemUser); +($id, $msg) = $u10->Create(Name => 'CreateTest10'.$$, EmailAddress => $$.'create-test10}@[.com'); +ok (!$id, $msg); +RT->Config->Set('ValidateUserEmailAddresses' => undef); + +} + +{ + + +ok(my $user = RT::User->new($RT::SystemUser)); +ok($user->Load('root'), "Loaded user 'root'"); +ok($user->Privileged, "User 'root' is privileged"); +ok(my ($v,$m) = $user->SetPrivileged(0)); +is ($v ,1, "Set unprivileged suceeded ($m)"); +ok(!$user->Privileged, "User 'root' is no longer privileged"); +ok(my ($v2,$m2) = $user->SetPrivileged(1)); +is ($v2 ,1, "Set privileged suceeded ($m2"); +ok($user->Privileged, "User 'root' is privileged again"); + + +} + +{ + +ok(my $u = RT::User->new($RT::SystemUser)); +ok($u->Load(1), "Loaded the first user"); +is($u->PrincipalObj->ObjectId , 1, "user 1 is the first principal"); +is($u->PrincipalObj->PrincipalType, 'User' , "Principal 1 is a user, not a group"); + + +} + +{ + +my $root = RT::User->new($RT::SystemUser); +$root->Load('root'); +ok($root->Id, "Found the root user"); +my $rootq = RT::Queue->new($root); +$rootq->Load(1); +ok($rootq->Id, "Loaded the first queue"); + +ok ($rootq->CurrentUser->HasRight(Right=> 'CreateTicket', Object => $rootq), "Root can create tickets"); + +my $new_user = RT::User->new($RT::SystemUser); +my ($id, $msg) = $new_user->Create(Name => 'ACLTest'.$$); + +ok ($id, "Created a new user for acl test $msg"); + +my $q = RT::Queue->new($new_user); +$q->Load(1); +ok($q->Id, "Loaded the first queue"); + + +ok (!$q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "Some random user doesn't have the right to create tickets"); +ok (my ($gval, $gmsg) = $new_user->PrincipalObj->GrantRight( Right => 'CreateTicket', Object => $q), "Granted the random user the right to create tickets"); +ok ($gval, "Grant succeeded - $gmsg"); + + +ok ($q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "The user can create tickets after we grant him the right"); +ok ( ($gval, $gmsg) = $new_user->PrincipalObj->RevokeRight( Right => 'CreateTicket', Object => $q), "revoked the random user the right to create tickets"); +ok ($gval, "Revocation succeeded - $gmsg"); +ok (!$q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "The user can't create tickets anymore"); + + + + + +# Create a ticket in the queue +my $new_tick = RT::Ticket->new($RT::SystemUser); +my ($tickid, $tickmsg) = $new_tick->Create(Subject=> 'ACL Test', Queue => 'General'); +ok($tickid, "Created ticket: $tickid"); +# Make sure the user doesn't have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); +# Create a new group +my $group = RT::Group->new($RT::SystemUser); +$group->CreateUserDefinedGroup(Name => 'ACLTest'.$$); +ok($group->Id, "Created a new group Ok"); +# Grant a group the right to modify tickets in a queue +ok(my ($gv,$gm) = $group->PrincipalObj->GrantRight( Object => $q, Right => 'ModifyTicket'),"Granted the group the right to modify tickets"); +ok($gv,"Grant succeeed - $gm"); +# Add the user to the group +ok( my ($aid, $amsg) = $group->AddMember($new_user->PrincipalId), "Added the member to the group"); +ok ($aid, "Member added to group: $amsg"); +# Make sure the user does have the right to modify tickets in the queue +ok ($new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can modify the ticket with group membership"); + + +# Remove the user from the group +ok( my ($did, $dmsg) = $group->DeleteMember($new_user->PrincipalId), "Deleted the member from the group"); +ok ($did,"Deleted the group member: $dmsg"); +# Make sure the user doesn't have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); + + +my $q_as_system = RT::Queue->new($RT::SystemUser); +$q_as_system->Load(1); +ok($q_as_system->Id, "Loaded the first queue"); + +# Create a ticket in the queue +my $new_tick2 = RT::Ticket->new($RT::SystemUser); +(my $tick2id, $tickmsg) = $new_tick2->Create(Subject=> 'ACL Test 2', Queue =>$q_as_system->Id); +ok($tick2id, "Created ticket: $tick2id"); +is($new_tick2->QueueObj->id, $q_as_system->Id, "Created a new ticket in queue 1"); + + +# make sure that the user can't do this without subgroup membership +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); + +# Create a subgroup +my $subgroup = RT::Group->new($RT::SystemUser); +$subgroup->CreateUserDefinedGroup(Name => 'Subgrouptest'.$$); +ok($subgroup->Id, "Created a new group ".$subgroup->Id."Ok"); +#Add the subgroup as a subgroup of the group +my ($said, $samsg) = $group->AddMember($subgroup->PrincipalId); +ok ($said, "Added the subgroup as a member of the group"); +# Add the user to a subgroup of the group + +my ($usaid, $usamsg) = $subgroup->AddMember($new_user->PrincipalId); +ok($usaid,"Added the user ".$new_user->Id."to the subgroup"); +# Make sure the user does have the right to modify tickets in the queue +ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket with subgroup membership"); + +# {{{ Deal with making sure that members of subgroups of a disabled group don't have rights + +($id, $msg) = $group->SetDisabled(1); +ok ($id,$msg); +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket when the group ".$group->Id. " is disabled"); + ($id, $msg) = $group->SetDisabled(0); +ok($id,$msg); +# Test what happens when we disable the group the user is a member of directly + +($id, $msg) = $subgroup->SetDisabled(1); + ok ($id,$msg); +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket when the group ".$subgroup->Id. " is disabled"); + ($id, $msg) = $subgroup->SetDisabled(0); + ok ($id,$msg); +ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket without group membership"); + +# }}} + + +my ($usrid, $usrmsg) = $subgroup->DeleteMember($new_user->PrincipalId); +ok($usrid,"removed the user from the group - $usrmsg"); +# Make sure the user doesn't have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); + +#revoke the right to modify tickets in a queue +ok(($gv,$gm) = $group->PrincipalObj->RevokeRight( Object => $q, Right => 'ModifyTicket'),"Granted the group the right to modify tickets"); +ok($gv,"revoke succeeed - $gm"); + +# {{{ Test the user's right to modify a ticket as a _queue_ admincc for a right granted at the _queue_ level + +# Grant queue admin cc the right to modify ticket in the queue +ok(my ($qv,$qm) = $q_as_system->AdminCc->PrincipalObj->GrantRight( Object => $q_as_system, Right => 'ModifyTicket'),"Granted the queue adminccs the right to modify tickets"); +ok($qv, "Granted the right successfully - $qm"); + +# Add the user as a queue admincc +ok (my ($add_id, $add_msg) = $q_as_system->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); +ok ($add_id, "the user is now a queue admincc - $add_msg"); + +# Make sure the user does have the right to modify tickets in the queue +ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); +# Remove the user from the role group +ok (my ($del_id, $del_msg) = $q_as_system->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); + +# Make sure the user doesn't have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); + +# }}} + +# {{{ Test the user's right to modify a ticket as a _ticket_ admincc with the right granted at the _queue_ level + +# Add the user as a ticket admincc +ok (my( $uadd_id, $uadd_msg) = $new_tick2->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); +ok ($add_id, "the user is now a queue admincc - $add_msg"); + +# Make sure the user does have the right to modify tickets in the queue +ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); + +# Remove the user from the role group +ok (( $del_id, $del_msg) = $new_tick2->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); + +# Make sure the user doesn't have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); + + +# Revoke the right to modify ticket in the queue +ok(my ($rqv,$rqm) = $q_as_system->AdminCc->PrincipalObj->RevokeRight( Object => $q_as_system, Right => 'ModifyTicket'),"Revokeed the queue adminccs the right to modify tickets"); +ok($rqv, "Revoked the right successfully - $rqm"); + +# }}} + + + +# {{{ Test the user's right to modify a ticket as a _queue_ admincc for a right granted at the _system_ level + +# Before we start Make sure the user does not have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without it being granted"); +ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue without it being granted"); + +# Grant queue admin cc the right to modify ticket in the queue +ok(($qv,$qm) = $q_as_system->AdminCc->PrincipalObj->GrantRight( Object => $RT::System, Right => 'ModifyTicket'),"Granted the queue adminccs the right to modify tickets"); +ok($qv, "Granted the right successfully - $qm"); + +# Make sure the user can't modify the ticket before they're added as a watcher +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without being an admincc"); +ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue without being an admincc"); + +# Add the user as a queue admincc +ok (($add_id, $add_msg) = $q_as_system->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); +ok ($add_id, "the user is now a queue admincc - $add_msg"); + +# Make sure the user does have the right to modify tickets in the queue +ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); +ok ($new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can modify tickets in the queue as an admincc"); +# Remove the user from the role group +ok (($del_id, $del_msg) = $q_as_system->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); + +# Make sure the user doesn't have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership"); +ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can't modify tickets in the queue without group membership"); + +# }}} + +# {{{ Test the user's right to modify a ticket as a _ticket_ admincc with the right granted at the _queue_ level + +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without being an admincc"); +ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj without being an admincc"); + + +# Add the user as a ticket admincc +ok ( ($uadd_id, $uadd_msg) = $new_tick2->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc"); +ok ($add_id, "the user is now a queue admincc - $add_msg"); + +# Make sure the user does have the right to modify tickets in the queue +ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc"); +ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj being only a ticket admincc"); + +# Remove the user from the role group +ok ( ($del_id, $del_msg) = $new_tick2->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc"); + +# Make sure the user doesn't have the right to modify tickets in the queue +ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without being an admincc"); +ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj without being an admincc"); + + +# Revoke the right to modify ticket in the queue +ok(($rqv,$rqm) = $q_as_system->AdminCc->PrincipalObj->RevokeRight( Object => $RT::System, Right => 'ModifyTicket'),"Revokeed the queue adminccs the right to modify tickets"); +ok($rqv, "Revoked the right successfully - $rqm"); + +# }}} + + + + +# Grant "privileged users" the system right to create users +# Create a privileged user. +# have that user create another user +# Revoke the right for privileged users to create users +# have the privileged user try to create another user and fail the ACL check + + +} + +1; diff --git a/rt/t/api/users.t b/rt/t/api/users.t new file mode 100644 index 000000000..d1ff174e1 --- /dev/null +++ b/rt/t/api/users.t @@ -0,0 +1,80 @@ + +use strict; +use warnings; +use RT; +use RT::Test tests => 11; + + +{ + +ok(require RT::Users); + + +} + +{ + no warnings qw(redefine once); + +ok(my $users = RT::Users->new($RT::SystemUser)); +$users->WhoHaveRight(Object =>$RT::System, Right =>'SuperUser'); +is($users->Count , 1, "There is one privileged superuser - Found ". $users->Count ); +# TODO: this wants more testing + +my $RTxUser = RT::User->new($RT::SystemUser); +my ($id, $msg) = $RTxUser->Create( Name => 'RTxUser', Comments => "RTx extension user", Privileged => 1); +ok ($id,$msg); + +my $group = RT::Group->new($RT::SystemUser); +$group->LoadACLEquivalenceGroup($RTxUser->PrincipalObj); + +my $RTxSysObj = {}; +bless $RTxSysObj, 'RTx::System'; +*RTx::System::Id = sub { 1; }; +*RTx::System::id = *RTx::System::Id; +my $ace = RT::Record->new($RT::SystemUser); +$ace->Table('ACL'); +$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)}); +($id, $msg) = $ace->Create( PrincipalId => $group->id, PrincipalType => 'Group', RightName => 'RTxUserRight', ObjectType => 'RTx::System', ObjectId => 1 ); +ok ($id, "ACL for RTxSysObj created"); + +my $RTxObj = {}; +bless $RTxObj, 'RTx::System::Record'; +*RTx::System::Record::Id = sub { 4; }; +*RTx::System::Record::id = *RTx::System::Record::Id; + +$users = RT::Users->new($RT::SystemUser); +$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxSysObj); +is($users->Count, 1, "RTxUserRight found for RTxSysObj"); + +$users = RT::Users->new($RT::SystemUser); +$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj); +is($users->Count, 0, "RTxUserRight not found for RTxObj"); + +$users = RT::Users->new($RT::SystemUser); +$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj, EquivObjects => [ $RTxSysObj ]); +is($users->Count, 1, "RTxUserRight found for RTxObj using EquivObjects"); + +$ace = RT::Record->new($RT::SystemUser); +$ace->Table('ACL'); +$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)}); +($id, $msg) = $ace->Create( PrincipalId => $group->id, PrincipalType => 'Group', RightName => 'RTxUserRight', ObjectType => 'RTx::System::Record', ObjectId => 5 ); +ok ($id, "ACL for RTxObj created"); + +my $RTxObj2 = {}; +bless $RTxObj2, 'RTx::System::Record'; +*RTx::System::Record::Id = sub { 5; }; +*RTx::System::Record::id = sub { 5; }; + +$users = RT::Users->new($RT::SystemUser); +$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj2); +is($users->Count, 1, "RTxUserRight found for RTxObj2"); + +$users = RT::Users->new($RT::SystemUser); +$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj2, EquivObjects => [ $RTxSysObj ]); +is($users->Count, 1, "RTxUserRight found for RTxObj2"); + + + +} + +1; diff --git a/rt/t/approval/basic.t b/rt/t/approval/basic.t new file mode 100644 index 000000000..597459125 --- /dev/null +++ b/rt/t/approval/basic.t @@ -0,0 +1,218 @@ + +use strict; +use warnings; +use Test::More; +BEGIN { + eval { require Email::Abstract; require Test::Email; 1 } + or plan skip_all => 'require Email::Abstract and Test::Email'; +} + + +use RT; +use RT::Test tests => 39; +use RT::Test::Email; + +RT->Config->Set( LogToScreen => 'debug' ); +RT->Config->Set( UseTransactionBatch => 1 ); +my ($baseurl, $m) = RT::Test->started_ok; + +my $q = RT::Queue->new($RT::SystemUser); +$q->Load('___Approvals'); +$q->SetDisabled(0); + +my %users; +for my $user_name (qw(minion cfo ceo )) { + my $user = $users{$user_name} = RT::User->new($RT::SystemUser); + $user->Create( Name => uc($user_name), + Privileged => 1, + EmailAddress => $user_name.'@company.com'); + my ($val, $msg); + ($val, $msg) = $user->PrincipalObj->GrantRight(Object =>$q, Right => $_) + for qw(ModifyTicket OwnTicket ShowTicket); +} + +# XXX: we need to make the first approval ticket open so notification is sent. +my $approvals = +'===Create-Ticket: for-CFO +Queue: ___Approvals +Type: approval +Owner: CFO +Requestors: {$Tickets{"TOP"}->Requestors} +Refers-To: TOP +Subject: CFO Approval for PO: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject} +Due: {time + 86400} +Content-Type: text/plain +Content: Your approval is requested for the PO ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject} +Blah +Blah +ENDOFCONTENT +===Create-Ticket: for-CEO +Queue: ___Approvals +Type: approval +Owner: CEO +Requestors: {$Tickets{"TOP"}->Requestors} +Subject: PO approval request for {$Tickets{"TOP"}->Subject} +Refers-To: TOP +Depends-On: for-CFO +Depended-On-By: {$Tickets{"TOP"}->Id} +Content-Type: text/plain +Content: +Your CFO approved PO ticket {$Tickets{"TOP"}->Id} for minion. you ok with that? +ENDOFCONTENT +'; + +my $apptemp = RT::Template->new($RT::SystemUser); +$apptemp->Create( Content => $approvals, Name => "PO Approvals", Queue => "0"); + +ok($apptemp->Id); + +$q = RT::Queue->new($RT::SystemUser); +$q->Create(Name => 'PO'); +ok ($q->Id, "Created PO queue"); + +my $scrip = RT::Scrip->new($RT::SystemUser); +my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Create', + ScripAction => 'Create Tickets', + Template => 'PO Approvals', + Queue => $q->Id, + Description => 'Create Approval Tickets'); +ok ($sval, $smsg); +ok ($scrip->Id, "Created the scrip"); +ok ($scrip->TemplateObj->Id, "Created the scrip template"); +ok ($scrip->ConditionObj->Id, "Created the scrip condition"); +ok ($scrip->ActionObj->Id, "Created the scrip action"); + +my $t = RT::Ticket->new($RT::SystemUser); +my ($tid, $ttrans, $tmsg); + +mail_ok { + ($tid, $ttrans, $tmsg) = + $t->Create(Subject => "PO for stationary", + Owner => "root", Requestor => 'minion', + Queue => $q->Id); +} { from => qr/RT System/, + to => 'cfo@company.com', + subject => qr/New Pending Approval: CFO Approval/, + body => qr/pending your approval.*Your approval is requested.*Blah/s +},{ from => qr/PO via RT/, + to => 'minion@company.com', + subject => qr/PO for stationary/, + body => qr/automatically generated in response/ +}; + +ok ($tid,$tmsg); + +is ($t->ReferredToBy->Count,2, "referred to by the two tickets"); + +my $deps = $t->DependsOn; +is ($deps->Count, 1, "The ticket we created depends on one other ticket"); +my $dependson_ceo= $deps->First->TargetObj; +ok ($dependson_ceo->Id, "It depends on a real ticket"); +like($dependson_ceo->Subject, qr/PO approval request.*stationary/); + +$deps = $dependson_ceo->DependsOn; +is ($deps->Count, 1, "The ticket we created depends on one other ticket"); +my $dependson_cfo = $deps->First->TargetObj; +ok ($dependson_cfo->Id, "It depends on a real ticket"); + +like($dependson_cfo->Subject, qr/CFO Approval for PO.*stationary/); + +is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ], + [ 'new', 'open', 'new'], 'tickets in correct state'); + +mail_ok { + my $cfo = RT::CurrentUser->new; + $cfo->Load( $users{cfo} ); + + $dependson_cfo->CurrentUser($cfo); + my $notes = MIME::Entity->build( + Data => [ 'Resources exist to be consumed.' ] + ); + RT::I18N::SetMIMEEntityToUTF8($notes); # convert text parts into utf-8 + + my ( $notesval, $notesmsg ) = $dependson_cfo->Correspond( MIMEObj => $notes ); + ok($notesval, $notesmsg); + + my ($ok, $msg) = $dependson_cfo->SetStatus( Status => 'resolved' ); + ok($ok, "cfo can approve - $msg"); + +} { from => qr/RT System/, + to => 'ceo@company.com', + subject => qr/New Pending Approval: PO approval request for PO/, + body => qr/pending your approval.*CFO approved.*ok with that\?/s +},{ from => qr/RT System/, + to => 'minion@company.com', + subject => qr/Ticket Approved:/, + body => qr/approved by CFO.*notes: Resources exist to be consumed/s +}; + +is ($t->DependsOn->Count, 1, "still depends only on the CEO approval"); +is ($t->ReferredToBy->Count,2, "referred to by the two tickets"); + +is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ], + [ 'new', 'resolved', 'open'], 'ticket state after cfo approval'); + +mail_ok { + my $ceo = RT::CurrentUser->new; + $ceo->Load( $users{ceo} ); + + $dependson_ceo->CurrentUser($ceo); + my $notes = MIME::Entity->build( + Data => [ 'And consumed they will be.' ] + ); + RT::I18N::SetMIMEEntityToUTF8($notes); # convert text parts into utf-8 + + my ( $notesval, $notesmsg ) = $dependson_ceo->Correspond( MIMEObj => $notes ); + ok($notesval, $notesmsg); + + my ($ok, $msg) = $dependson_ceo->SetStatus( Status => 'resolved' ); + ok($ok, "ceo can approve - $msg"); + +} { from => qr/RT System/, + to => 'minion@company.com', + subject => qr/Ticket Approved:/, + body => qr/approved by CEO.*Its Owner may now start to act on it.*notes: And consumed they will be/s, +}, { from => qr'CEO via RT', + to => 'root@localhost', + subject => qr/Ticket Approved/, + body => qr/The ticket has been approved, you may now start to act on it/, +}; + + +is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ], + [ 'new', 'resolved', 'resolved'], 'ticket state after ceo approval'); + +$dependson_cfo->_Set( + Field => 'Status', + Value => 'open'); + +$dependson_ceo->_Set( + Field => 'Status', + Value => 'new'); + +mail_ok { + my $cfo = RT::CurrentUser->new; + $cfo->Load( $users{cfo} ); + + $dependson_cfo->CurrentUser($cfo); + my $notes = MIME::Entity->build( + Data => [ 'sorry, out of resources.' ] + ); + RT::I18N::SetMIMEEntityToUTF8($notes); # convert text parts into utf-8 + + my ( $notesval, $notesmsg ) = $dependson_cfo->Correspond( MIMEObj => $notes ); + ok($notesval, $notesmsg); + + my ($ok, $msg) = $dependson_cfo->SetStatus( Status => 'rejected' ); + ok($ok, "cfo can approve - $msg"); + +} { from => qr/RT System/, + to => 'minion@company.com', + subject => qr/Ticket Rejected: PO for stationary/, + body => qr/rejected by CFO.*out of resources/s, +}; + +$t->Load($t->id);$dependson_ceo->Load($dependson_ceo->id); +is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ], + [ 'rejected', 'rejected', 'deleted'], 'ticket state after cfo rejection'); + diff --git a/rt/t/clicky.t b/rt/t/clicky.t new file mode 100644 index 000000000..8d5227e04 --- /dev/null +++ b/rt/t/clicky.t @@ -0,0 +1,119 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Test::More; +use RT::Test tests => 14; +my %clicky; + +BEGIN { + + %clicky = map { $_ => 1 } grep $_, RT->Config->Get('Active_MakeClicky'); + +# this's hack: we have to use RT::Test first to get RT->Config work, this +# results in the fact that we can't plan any more + unless ( keys %clicky ) { + SKIP: { + skip "No active Make Clicky actions", 14; + } + exit 0; + } +} + +my ($baseurl, $m) = RT::Test->started_ok; + +use_ok('MIME::Entity'); + +my $CurrentUser = $RT::SystemUser; + +my $queue = new RT::Queue($CurrentUser); +$queue->Load('General') || Abort(loc("Queue could not be loaded.")); + +my $message = MIME::Entity->build( + Subject => 'test', + Data => <<END, +If you have some problems with RT you could find help +on http://wiki.bestpractical.com or subscribe to +the rt-users\@lists.bestpractical.com. + +-- +Best regards. BestPractical Team. +END +); + +my $ticket = new RT::Ticket( $CurrentUser ); +my ($id) = $ticket->Create( + Subject => 'test', + Queue => $queue->Id, + MIMEObj => $message, +); +ok($id, "We created a ticket #$id"); +ok($ticket->Transactions->First->Content, "Has some content"); + +ok $m->login, 'logged in'; +ok $m->goto_ticket($id), 'opened diplay page of the ticket'; + +SKIP: { + skip "httpurl action disabled", 1 unless $clicky{'httpurl'}; + my @links = $m->find_link( + tag => 'a', + url => 'http://wiki.bestpractical.com', + text => 'Open URL', + ); + ok( scalar @links, 'found clicky link' ); +} + +SKIP: { + skip "httpurl_overwrite action disabled", 1 unless $clicky{'httpurl_overwrite'}; + my @links = $m->find_link( + tag => 'a', + url => 'http://wiki.bestpractical.com', + text => 'http://wiki.bestpractical.com', + ); + ok( scalar @links, 'found clicky link' ); +} + +{ + +my $message = MIME::Entity->build( + Type => 'text/html', + Subject => 'test', + Data => <<END, +If you have some problems with RT you could find help +on <a href="http://wiki.bestpractical.com">wiki</a> +or find known bugs on http://rt3.fsck.com +-- +Best regards. BestPractical Team. +END +); + +my $ticket = new RT::Ticket($CurrentUser); +my ($id) = $ticket->Create( + Subject => 'test', + Queue => $queue->Id, + MIMEObj => $message, +); +ok( $id, "We created a ticket #$id" ); +ok( $ticket->Transactions->First->Content, "Has some content" ); + +ok $m->login, 'logged in'; +ok $m->goto_ticket($id), 'opened diplay page of the ticket'; + +SKIP: { + skip "httpurl action disabled", 2 unless $clicky{'httpurl'}; + my @links = $m->find_link( + tag => 'a', + url => 'http://wiki.bestpractical.com', + text => 'Open URL', + ); + ok( @links == 0, 'not make clicky links clicky twice' ); + + @links = $m->find_link( + tag => 'a', + url => 'http://rt3.fsck.com', + text => 'Open URL', + ); + ok( scalar @links, 'found clicky link' ); +} + +} diff --git a/rt/t/cron.t b/rt/t/cron.t new file mode 100644 index 000000000..29d7bd7f6 --- /dev/null +++ b/rt/t/cron.t @@ -0,0 +1,90 @@ +#!/usr/bin/perl -w + +use strict; + +use RT; +use RT::Test tests => 18; + + +### Set up some testing data. Test the testing data because why not? + +# Create a user with rights, a queue, and some tickets. +my $user_obj = RT::User->new($RT::SystemUser); +my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('tara@example.com'); +ok($ret, 'record test user creation'); +$user_obj->SetName('tara'); +$user_obj->PrincipalObj->GrantRight(Right => 'SuperUser'); +my $CurrentUser = RT::CurrentUser->new('tara'); + +# Create our template, which will be used for tests of RT::Action::Record*. + +my $template_content = 'RT-Send-Cc: tla@example.com +RT-Send-Bcc: jesse@example.com + +This is a content string with no content.'; + +my $template_obj = RT::Template->new($CurrentUser); +$template_obj->Create(Queue => '0', + Name => 'recordtest', + Description => 'testing Record actions', + Content => $template_content, + ); + +# Create a queue and some tickets. + +my $queue_obj = RT::Queue->new($CurrentUser); +($ret, $msg) = $queue_obj->Create(Name => 'recordtest', Description => 'queue for Action::Record testing'); +ok($ret, 'record test queue creation'); + +my $ticket1 = RT::Ticket->new($CurrentUser); +my ($id, $tobj, $msg2) = $ticket1->Create(Queue => $queue_obj, + Requestor => ['tara@example.com'], + Subject => 'bork bork bork', + Priority => 22, + ); +ok($id, 'record test ticket creation 1'); +my $ticket2 = RT::Ticket->new($CurrentUser); +($id, $tobj, $msg2) = $ticket2->Create(Queue => $queue_obj, + Requestor => ['root@localhost'], + Subject => 'hurdy gurdy' + ); +ok($id, 'record test ticket creation 2'); + + +### OK. Have data, will travel. + +# First test the search. + +ok(require RT::Search::FromSQL, "Search::FromSQL loaded"); +my $ticketsqlstr = "Requestor.EmailAddress = '" . $CurrentUser->EmailAddress . + "' AND Priority > '20'"; +my $search = RT::Search::FromSQL->new(Argument => $ticketsqlstr, TicketsObj => RT::Tickets->new($CurrentUser), + ); +is(ref($search), 'RT::Search::FromSQL', "search created"); +ok($search->Prepare(), "fromsql search run"); +my $counter = 0; +while(my $t = $search->TicketsObj->Next() ) { + is($t->Id(), $ticket1->Id(), "fromsql search results 1"); + $counter++; +} +is ($counter, 1, "fromsql search results 2"); + +# Right. Now test the actions. + +ok(require RT::Action::RecordComment); +ok(require RT::Action::RecordCorrespondence); + +my ($comment_act, $correspond_act); +ok($comment_act = RT::Action::RecordComment->new(TicketObj => $ticket1, TemplateObj => $template_obj, CurrentUser => $CurrentUser), "RecordComment created"); +ok($correspond_act = RT::Action::RecordCorrespondence->new(TicketObj => $ticket2, TemplateObj => $template_obj, CurrentUser => $CurrentUser), "RecordCorrespondence created"); +ok($comment_act->Prepare(), "Comment prepared"); +ok($correspond_act->Prepare(), "Correspond prepared"); +ok($comment_act->Commit(), "Comment committed"); +ok($correspond_act->Commit(), "Correspondence committed"); + +# Now test for loop suppression. +my ($trans, $desc, $transaction) = $ticket2->Comment(MIMEObj => $template_obj->MIMEObj); +my $bogus_action = RT::Action::RecordComment->new(TicketObj => $ticket1, TemplateObj => $template_obj, TransactionObj => $transaction, CurrentUser => $CurrentUser); +ok(!$bogus_action->Prepare(), "Comment aborted to prevent loop"); + +1; diff --git a/rt/t/customfields/access_via_queue.t b/rt/t/customfields/access_via_queue.t new file mode 100644 index 000000000..c291860ea --- /dev/null +++ b/rt/t/customfields/access_via_queue.t @@ -0,0 +1,160 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 35; +use RT::Ticket; +use RT::CustomField; + +my $queue_name = "CFSortQueue-$$"; +my $queue = RT::Test->load_or_create_queue( Name => $queue_name ); +ok($queue && $queue->id, "$queue_name - test queue creation"); + +diag "create a CF\n" if $ENV{TEST_VERBOSE}; +my $cf_name = "Rights$$"; +my $cf; +{ + $cf = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $cf->Create( + Name => $cf_name, + Queue => $queue->id, + Type => 'FreeformSingle', + ); + ok($ret, "Custom Field Order created"); +} + +my $tester = RT::Test->load_or_create_user( + Name => 'tester', Password => 'password', +); +ok $tester && $tester->id, 'loaded or created user'; + +my $cc_role = RT::Group->new( $queue->CurrentUser ); +$cc_role->LoadQueueRoleGroup( Type => 'Cc', Queue => $queue->id ); + +my $owner_role = RT::Group->new( $queue->CurrentUser ); +$owner_role->LoadQueueRoleGroup( Type => 'Owner', Queue => $queue->id ); + +ok( RT::Test->set_rights( + { Principal => $tester, Right => [qw(SeeQueue ShowTicket CreateTicket ReplyToTicket Watch OwnTicket TakeTicket)] }, + { Principal => $cc_role, Object => $queue, Right => [qw(SeeCustomField)] }, + { Principal => $owner_role, Object => $queue, Right => [qw(ModifyCustomField)] }, +), 'set rights'); + +{ + my $ticket = RT::Ticket->new( $tester ); + my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test' ); + ok $tid, "created ticket"; + + ok !$ticket->CustomFields->First, "see no fields"; +} + +{ + my $ticket = RT::Ticket->new( $tester ); + my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id ); + ok $tid, "created ticket"; + + my $cf = $ticket->CustomFields->First; + ok $cf, "Ccs see cf"; +} + +{ + my $ticket = RT::Ticket->new( $tester ); + my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id ); + ok $tid, "created ticket"; + + (my $status, $msg) = $ticket->AddCustomFieldValue( Field => $cf->Name, Value => 'test' ); + ok !$status, "Can not change CF"; +} + +{ + my $ticket = RT::Ticket->new( $tester ); + my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id, Owner => $tester->id ); + ok $tid, "created ticket"; + + (my $status, $msg) = $ticket->AddCustomFieldValue( Field => $cf->Name, Value => 'test' ); + ok $status, "Changed CF"; + is $ticket->FirstCustomFieldValue( $cf->Name ), 'test'; + + ($status, $msg) = $ticket->DeleteCustomFieldValue( Field => $cf->Name, Value => 'test' ); + ok $status, "Changed CF"; + is $ticket->FirstCustomFieldValue( $cf->Name ), undef; +} + +{ + my $ticket = RT::Ticket->new( $tester ); + my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id, Owner => $tester->id ); + ok $tid, "created ticket"; + + (my $status, $msg) = $ticket->AddCustomFieldValue( Field => $cf->id, Value => 'test' ); + ok $status, "Changed CF"; + is $ticket->FirstCustomFieldValue( $cf->id ), 'test'; + + ($status, $msg) = $ticket->DeleteCustomFieldValue( Field => $cf->id, Value => 'test' ); + ok $status, "Changed CF"; + is $ticket->FirstCustomFieldValue( $cf->id ), undef; +} + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login( tester => 'password' ), 'logged in'; + +diag "check that we have no the CF on the create" if $ENV{'TEST_VERBOSE'}; +{ + $m->submit_form( + form_name => "CreateTicketInQueue", + fields => { Queue => $queue->Name }, + ); + + my $form = $m->form_name("TicketCreate"); + my $cf_field = "Object-RT::Ticket--CustomField-". $cf->id ."-Value"; + ok !$form->find_input( $cf_field ), 'no form field on the page'; + + $m->submit_form( + form_name => "TicketCreate", + fields => { Subject => 'test' }, + ); + my ($tid) = ($m->content =~ /Ticket (\d+) created/i); + ok $tid, "created a ticket succesfully"; + $m->content_unlike(qr/$cf_name/, "don't see CF"); + + $m->follow_link( text => 'Custom Fields' ); + $form = $m->form_number(3); + $cf_field = "Object-RT::Ticket-$tid-CustomField-". $cf->id ."-Value"; + ok !$form->find_input( $cf_field ), 'no form field on the page'; +} + +diag "check that we see CF as Cc" if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $tester ); + my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id ); + ok $tid, "created ticket"; + + ok $m->goto_ticket( $tid ), "opened ticket"; + $m->content_like(qr/$cf_name/, "see CF"); +} + +diag "check that owner can see and edit CF" if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $tester ); + my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id, Owner => $tester->id ); + ok $tid, "created ticket"; + + ok $m->goto_ticket( $tid ), "opened ticket"; + $m->content_like(qr/$cf_name/, "see CF"); + + $m->follow_link( text => 'Custom Fields' ); + my $form = $m->form_number(3); + my $cf_field = "Object-RT::Ticket-$tid-CustomField-". $cf->id ."-Value"; + ok $form->find_input( $cf_field ), 'form field on the page'; + + $m->submit_form( + form_number => 3, + fields => { + $cf_field => "changed cf", + }, + ); + + ok $m->goto_ticket( $tid ), "opened ticket"; + $m->content_like(qr/$cf_name/, "changed cf"); +} + diff --git a/rt/t/customfields/sort_order.t b/rt/t/customfields/sort_order.t new file mode 100644 index 000000000..472eb6bf3 --- /dev/null +++ b/rt/t/customfields/sort_order.t @@ -0,0 +1,92 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 18; +use RT::Ticket; +use RT::CustomField; + +my $queue_name = "CFSortQueue-$$"; +my $queue = RT::Test->load_or_create_queue( Name => $queue_name ); +ok($queue && $queue->id, "$queue_name - test queue creation"); + +diag "create multiple CFs: B, A and C" if $ENV{TEST_VERBOSE}; +my @cfs = (); +{ + my $cf = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $cf->Create( + Name => "CF B", + Queue => $queue->id, + Type => 'FreeformSingle', + ); + ok($ret, "Custom Field Order created"); + push @cfs, $cf; +} +{ + my $cf = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $cf->Create( + Name => "CF A", + Queue => $queue->id, + Type => 'FreeformSingle', + ); + ok($ret, "Custom Field Order created"); + push @cfs, $cf; +} +{ + my $cf = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $cf->Create( + Name => "CF C", + Queue => $queue->id, + Type => 'FreeformSingle', + ); + ok($ret, "Custom Field Order created"); + push @cfs, $cf; +} + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login( root => 'password' ), 'logged in'; + +diag "reorder CFs: C, A and B" if $ENV{TEST_VERBOSE}; +{ + $m->get( '/Admin/Queues/' ); + $m->follow_link_ok( {text => $queue->id} ); + $m->follow_link_ok( {text => 'Ticket Custom Fields'} ); + + my @tmp = ($m->content =~ /(CF [ABC])/g); + is_deeply(\@tmp, ['CF B', 'CF A', 'CF C']); + + $m->follow_link_ok( {text => 'Move up', n => 2} ); + $m->follow_link_ok( {text => 'Move up', n => 1} ); + $m->follow_link_ok( {text => 'Move up', n => 2} ); + + @tmp = ($m->content =~ /(CF [ABC])/g); + is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']); +} + +diag "check ticket create, display and edit pages" if $ENV{TEST_VERBOSE}; +{ + $m->submit_form( + form_name => "CreateTicketInQueue", + fields => { Queue => $queue->Name }, + ); + + my @tmp = ($m->content =~ /(CF [ABC])/g); + is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']); + + $m->submit_form( + form_name => "TicketCreate", + fields => { Subject => 'test' }, + ); + my ($tid) = ($m->content =~ /Ticket (\d+) created/i); + ok $tid, "created a ticket succesfully"; + + @tmp = ($m->content =~ /(CF [ABC])/g); + is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']); + + $m->follow_link_ok( {text => 'Custom Fields'} ); + + @tmp = ($m->content =~ /(CF [ABC])/g); + is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']); +} + diff --git a/rt/t/data/configs/apache2.2+fastcgi.conf b/rt/t/data/configs/apache2.2+fastcgi.conf new file mode 100644 index 000000000..f9b3b1034 --- /dev/null +++ b/rt/t/data/configs/apache2.2+fastcgi.conf @@ -0,0 +1,44 @@ +ServerRoot %%SERVER_ROOT%% +PidFile %%PID_FILE%% +ServerAdmin root@localhost + +%%LOAD_MODULES%% + +TypesConfig /etc/mime.types +FastCgiIpcDir /tmp + +<IfModule !mpm_netware_module> +<IfModule !mpm_winnt_module> +User www +Group www +</IfModule> +</IfModule> + +Listen %%LISTEN%% + +ErrorLog "%%LOG_FILE%%" +LogLevel debug + +<Directory /> + Options FollowSymLinks + AllowOverride None + Order deny,allow + Deny from all +</Directory> + +AddDefaultCharset UTF-8 + +FastCgiServer %%RT_BIN_PATH%%/mason_handler.fcgi -processes 1 -idle-timeout 180 -initial-env RT_SITE_CONFIG=%%RT_SITE_CONFIG%% + +Alias /NoAuth/images/ %%DOCUMENT_ROOT%%/NoAuth/images/ +ScriptAlias / %%RT_BIN_PATH%%/mason_handler.fcgi/ + +DocumentRoot "%%DOCUMENT_ROOT%%" +<Location /> + Order allow,deny + Allow from all + + Options +ExecCGI + AddHandler fastcgi-script fcgi +</Location> + diff --git a/rt/t/data/configs/apache2.2+fastcgi.conf.in b/rt/t/data/configs/apache2.2+fastcgi.conf.in new file mode 100644 index 000000000..c0c01d1a5 --- /dev/null +++ b/rt/t/data/configs/apache2.2+fastcgi.conf.in @@ -0,0 +1,44 @@ +ServerRoot %%SERVER_ROOT%% +PidFile %%PID_FILE%% +ServerAdmin root@localhost + +%%LOAD_MODULES%% + +TypesConfig /etc/mime.types +FastCgiIpcDir /tmp + +<IfModule !mpm_netware_module> +<IfModule !mpm_winnt_module> +User @WEB_USER@ +Group @WEB_GROUP@ +</IfModule> +</IfModule> + +Listen %%LISTEN%% + +ErrorLog "%%LOG_FILE%%" +LogLevel debug + +<Directory /> + Options FollowSymLinks + AllowOverride None + Order deny,allow + Deny from all +</Directory> + +AddDefaultCharset UTF-8 + +FastCgiServer %%RT_BIN_PATH%%/mason_handler.fcgi -processes 1 -idle-timeout 180 -initial-env RT_SITE_CONFIG=%%RT_SITE_CONFIG%% + +Alias /NoAuth/images/ %%DOCUMENT_ROOT%%/NoAuth/images/ +ScriptAlias / %%RT_BIN_PATH%%/mason_handler.fcgi/ + +DocumentRoot "%%DOCUMENT_ROOT%%" +<Location /> + Order allow,deny + Allow from all + + Options +ExecCGI + AddHandler fastcgi-script fcgi +</Location> + diff --git a/rt/t/data/configs/apache2.2+mod_perl.conf b/rt/t/data/configs/apache2.2+mod_perl.conf new file mode 100644 index 000000000..341ae5024 --- /dev/null +++ b/rt/t/data/configs/apache2.2+mod_perl.conf @@ -0,0 +1,40 @@ +ServerRoot %%SERVER_ROOT%% +PidFile %%PID_FILE%% +ServerAdmin root@localhost + +%%LOAD_MODULES%% + +<IfModule !mpm_netware_module> +<IfModule !mpm_winnt_module> +User www +Group www +</IfModule> +</IfModule> + +Listen %%LISTEN%% + +ErrorLog "%%LOG_FILE%%" +LogLevel debug + +<Directory /> + Options FollowSymLinks + AllowOverride None + Order deny,allow + Deny from all +</Directory> + +AddDefaultCharset UTF-8 +PerlSetEnv RT_SITE_CONFIG %%RT_SITE_CONFIG%% +PerlRequire %%RT_BIN_PATH%%/webmux.pl + +RedirectMatch permanent (.*)/$ $1/index.html + +DocumentRoot "%%DOCUMENT_ROOT%%" +<Directory "%%DOCUMENT_ROOT%%"> + Order allow,deny + Allow from all + + SetHandler perl-script + PerlResponseHandler RT::Mason +</Directory> + diff --git a/rt/t/data/configs/apache2.2+mod_perl.conf.in b/rt/t/data/configs/apache2.2+mod_perl.conf.in new file mode 100644 index 000000000..ed12c8690 --- /dev/null +++ b/rt/t/data/configs/apache2.2+mod_perl.conf.in @@ -0,0 +1,40 @@ +ServerRoot %%SERVER_ROOT%% +PidFile %%PID_FILE%% +ServerAdmin root@localhost + +%%LOAD_MODULES%% + +<IfModule !mpm_netware_module> +<IfModule !mpm_winnt_module> +User @WEB_USER@ +Group @WEB_GROUP@ +</IfModule> +</IfModule> + +Listen %%LISTEN%% + +ErrorLog "%%LOG_FILE%%" +LogLevel debug + +<Directory /> + Options FollowSymLinks + AllowOverride None + Order deny,allow + Deny from all +</Directory> + +AddDefaultCharset UTF-8 +PerlSetEnv RT_SITE_CONFIG %%RT_SITE_CONFIG%% +PerlRequire %%RT_BIN_PATH%%/webmux.pl + +RedirectMatch permanent (.*)/$ $1/index.html + +DocumentRoot "%%DOCUMENT_ROOT%%" +<Directory "%%DOCUMENT_ROOT%%"> + Order allow,deny + Allow from all + + SetHandler perl-script + PerlResponseHandler RT::Mason +</Directory> + diff --git a/rt/t/data/emails/8859-15-message-series/dir b/rt/t/data/emails/8859-15-message-series/dir new file mode 100755 index 000000000..b9f8ec3ba --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/dir @@ -0,0 +1,356 @@ +Return-Path: <rt-users-admin@lists.fsck.com> +Delivered-To: j@pallas.eruditorum.org +Received: from pallas.eruditorum.org (localhost [127.0.0.1]) + by pallas.eruditorum.org (Postfix) with ESMTP + id 72E3A111B3; Mon, 26 May 2003 14:50:14 -0400 (EDT) +Delivered-To: rt-users@pallas.eruditorum.org +Received: from mail-in-02.arcor-online.net (mail-in-02.arcor-online.net [151.189.21.42]) + by pallas.eruditorum.org (Postfix) with ESMTP id 15E761118D + for <rt-users@lists.fsck.com>; Mon, 26 May 2003 14:49:56 -0400 (EDT) +Received: from otdial-212-144-012-186.arcor-ip.net (otdial-212-144-011-024.arcor-ip.net [212.144.11.24]) + by mail-in-02.arcor-online.net (Postfix) with ESMTP + id 745EE15E87; Mon, 26 May 2003 20:53:15 +0200 (CEST) +From: Dirk Pape <pape-rt@inf.fu-berlin.de> +To: Jesse Vincent <jesse@bestpractical.com>, + rt-users <rt-users@lists.fsck.com> +Subject: Re: [rt-users] [rt-announce] Development Snapshot 3.0.2++ +Message-ID: <2147483647.1053982235@otdial-212-144-011-024.arcor-ip.net> +In-Reply-To: <2147483647.1053974498@[10.0.255.35]> +References: <20030523202405.GF23719@fsck.com> + <2147483647.1053974498@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="==========2147500486==========" +Sender: rt-users-admin@lists.fsck.com +Errors-To: rt-users-admin@lists.fsck.com +X-BeenThere: rt-users@lists.fsck.com +X-Mailman-Version: 2.0.12 +Precedence: bulk +List-Help: <mailto:rt-users-request@lists.fsck.com?subject=help> +List-Post: <mailto:rt-users@lists.fsck.com> +List-Subscribe: <http://lists.fsck.com/mailman/listinfo/rt-users>, + <mailto:rt-users-request@lists.fsck.com?subject=subscribe> +List-Id: For users of RT: Request Tracker <rt-users.lists.fsck.com> +List-Unsubscribe: <http://lists.fsck.com/mailman/listinfo/rt-users>, + <mailto:rt-users-request@lists.fsck.com?subject=unsubscribe> +List-Archive: <http://lists.fsck.com/pipermail/rt-users/> +Date: Mon, 26 May 2003 20:50:36 +0200 +X-Spam-Status: No, hits=-2.5 required=5.0 + tests=AWL,IN_REP_TO,KNOWN_MAILING_LIST,QUOTED_EMAIL_TEXT, + REFERENCES,REPLY_WITH_QUOTES + autolearn=ham version=2.55 +X-Spam-Level: +X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp) + +--==========2147500486========== +Content-Type: text/plain; charset=us-ascii; format=flowed +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Hello, + +here is the digest I forgot to attach. And I also forgot to say, that these +were the only messages after a restart of apache. + +The messages in the digest are the copies which I - for testing purpose - +allways queue into a mailbox just befor it is queued via rt-mailgate into +the rt-system. + +--Am Montag, 26. Mai 2003 18:41 Uhr +0200 schrieb Dirk Pape +<pape-rt@inf.fu-berlin.de>: + +> I attach a digest with mails I send one after another to the rt-system +> and they get queued into one queue, each as a new ticket. + + + + +--==========2147500486========== +Content-Type: multipart/digest; boundary="==========2147489407==========" + +--==========2147489407========== +Content-Type: message/rfc822; name="test _________" + +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 27591 invoked by uid 9804); 26 May 2003 18:10:50 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:10:46 +0200 +Received: (Qmail 27575 invoked from network); 26 May 2003 18:10:46 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:46 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKYe-0000Yi-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:10:44 +0200 +Received: (qmail 27557 invoked by uid 9804); 26 May 2003 18:10:44 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:10:40 +0200 +Received: (Qmail 27540 invoked from network); 26 May 2003 18:10:40 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:40 +0200 +Date: Mon, 26 May 2003 18:11:00 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972660@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27578] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + +--==========2147489407========== +Content-Type: message/rfc822; name="test _________" + +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 27754 invoked by uid 9804); 26 May 2003 18:11:24 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:20 +0200 +Received: (Qmail 27704 invoked from network); 26 May 2003 18:11:19 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:19 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKZA-0000Yy-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:16 +0200 +Received: (qmail 27690 invoked by uid 9804); 26 May 2003 18:11:16 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:13 +0200 +Received: (Qmail 27677 invoked from network); 26 May 2003 18:11:13 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:13 +0200 +Date: Mon, 26 May 2003 18:11:32 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972692@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27711] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + +--==========2147489407========== +Content-Type: message/rfc822; name="test _________" + +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 27971 invoked by uid 9804); 26 May 2003 18:12:02 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:52 +0200 +Received: (Qmail 27908 invoked from network); 26 May 2003 18:11:52 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:52 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKZj-0000ZC-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:51 +0200 +Received: (qmail 27848 invoked by uid 9804); 26 May 2003 18:11:50 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:46 +0200 +Received: (Qmail 27809 invoked from network); 26 May 2003 18:11:45 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:45 +0200 +Date: Mon, 26 May 2003 18:12:05 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972725@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27911] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + +--==========2147489407========== +Content-Type: message/rfc822; name="test _________" + +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 28283 invoked by uid 9804); 26 May 2003 18:12:39 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:12:36 +0200 +Received: (Qmail 28256 invoked from network); 26 May 2003 18:12:35 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:35 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKaQ-0000ZQ-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:12:34 +0200 +Received: (qmail 28236 invoked by uid 9804); 26 May 2003 18:12:34 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:12:30 +0200 +Received: (Qmail 28224 invoked from network); 26 May 2003 18:12:30 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:30 +0200 +Date: Mon, 26 May 2003 18:12:50 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972770@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28259] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + +--==========2147489407========== +Content-Type: message/rfc822; name="test _________" + +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 28578 invoked by uid 9804); 26 May 2003 18:13:20 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:13:15 +0200 +Received: (Qmail 28534 invoked from network); 26 May 2003 18:13:14 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:14 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKb1-0000Ze-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:13:11 +0200 +Received: (qmail 28516 invoked by uid 9804); 26 May 2003 18:13:11 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:13:08 +0200 +Received: (Qmail 28479 invoked from network); 26 May 2003 18:13:07 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:07 +0200 +Date: Mon, 26 May 2003 18:13:27 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972807@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28540] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + + +--==========2147489407========== +Content-Type: message/rfc822; name="test _________" + +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 29108 invoked by uid 9804); 26 May 2003 18:14:15 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:14:10 +0200 +Received: (Qmail 29066 invoked from network); 26 May 2003 18:14:10 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:10 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKbw-0000Zr-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:14:08 +0200 +Received: (qmail 29054 invoked by uid 9804); 26 May 2003 18:14:08 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:14:04 +0200 +Received: (Qmail 29036 invoked from network); 26 May 2003 18:14:04 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:04 +0200 +Date: Mon, 26 May 2003 18:14:24 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972864@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29069] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + +--==========2147489407========== +Content-Type: message/rfc822; name="test _________" + +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 29551 invoked by uid 9804); 26 May 2003 18:15:16 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:15:12 +0200 +Received: (Qmail 29521 invoked from network); 26 May 2003 18:15:12 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:12 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKcx-0000a4-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:15:11 +0200 +Received: (qmail 29511 invoked by uid 9804); 26 May 2003 18:15:10 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:15:07 +0200 +Received: (Qmail 29465 invoked from network); 26 May 2003 18:15:06 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:06 +0200 +Date: Mon, 26 May 2003 18:15:26 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972926@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29524] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + + +--==========2147489407==========-- + +--==========2147500486==========-- + +_______________________________________________ +rt-users mailing list +rt-users@lists.fsck.com +http://lists.fsck.com/mailman/listinfo/rt-users + +Have you read the FAQ? The RT FAQ Manager lives at http://fsck.com/rtfm + diff --git a/rt/t/data/emails/8859-15-message-series/msg1 b/rt/t/data/emails/8859-15-message-series/msg1 new file mode 100755 index 000000000..cc99c406c --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/msg1 @@ -0,0 +1,36 @@ +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 27591 invoked by uid 9804); 26 May 2003 18:10:50 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:10:46 +0200 +Received: (Qmail 27575 invoked from network); 26 May 2003 18:10:46 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:46 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKYe-0000Yi-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:10:44 +0200 +Received: (qmail 27557 invoked by uid 9804); 26 May 2003 18:10:44 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:10:40 +0200 +Received: (Qmail 27540 invoked from network); 26 May 2003 18:10:40 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:40 +0200 +Date: Mon, 26 May 2003 18:11:00 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972660@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27578] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + diff --git a/rt/t/data/emails/8859-15-message-series/msg2 b/rt/t/data/emails/8859-15-message-series/msg2 new file mode 100755 index 000000000..dc442cfc3 --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/msg2 @@ -0,0 +1,36 @@ +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 27754 invoked by uid 9804); 26 May 2003 18:11:24 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:20 +0200 +Received: (Qmail 27704 invoked from network); 26 May 2003 18:11:19 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:19 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKZA-0000Yy-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:16 +0200 +Received: (qmail 27690 invoked by uid 9804); 26 May 2003 18:11:16 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:13 +0200 +Received: (Qmail 27677 invoked from network); 26 May 2003 18:11:13 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:13 +0200 +Date: Mon, 26 May 2003 18:11:32 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972692@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27711] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + diff --git a/rt/t/data/emails/8859-15-message-series/msg3 b/rt/t/data/emails/8859-15-message-series/msg3 new file mode 100755 index 000000000..e23866d5f --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/msg3 @@ -0,0 +1,35 @@ +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 27971 invoked by uid 9804); 26 May 2003 18:12:02 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:52 +0200 +Received: (Qmail 27908 invoked from network); 26 May 2003 18:11:52 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:52 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKZj-0000ZC-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:51 +0200 +Received: (qmail 27848 invoked by uid 9804); 26 May 2003 18:11:50 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:11:46 +0200 +Received: (Qmail 27809 invoked from network); 26 May 2003 18:11:45 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:45 +0200 +Date: Mon, 26 May 2003 18:12:05 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972725@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27911] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 diff --git a/rt/t/data/emails/8859-15-message-series/msg4 b/rt/t/data/emails/8859-15-message-series/msg4 new file mode 100755 index 000000000..831695cc7 --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/msg4 @@ -0,0 +1,35 @@ +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 28283 invoked by uid 9804); 26 May 2003 18:12:39 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:12:36 +0200 +Received: (Qmail 28256 invoked from network); 26 May 2003 18:12:35 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:35 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKaQ-0000ZQ-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:12:34 +0200 +Received: (qmail 28236 invoked by uid 9804); 26 May 2003 18:12:34 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:12:30 +0200 +Received: (Qmail 28224 invoked from network); 26 May 2003 18:12:30 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:30 +0200 +Date: Mon, 26 May 2003 18:12:50 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972770@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28259] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 diff --git a/rt/t/data/emails/8859-15-message-series/msg5 b/rt/t/data/emails/8859-15-message-series/msg5 new file mode 100755 index 000000000..272c93c4f --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/msg5 @@ -0,0 +1,35 @@ +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 28578 invoked by uid 9804); 26 May 2003 18:13:20 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:13:15 +0200 +Received: (Qmail 28534 invoked from network); 26 May 2003 18:13:14 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:14 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKb1-0000Ze-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:13:11 +0200 +Received: (qmail 28516 invoked by uid 9804); 26 May 2003 18:13:11 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:13:08 +0200 +Received: (Qmail 28479 invoked from network); 26 May 2003 18:13:07 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:07 +0200 +Date: Mon, 26 May 2003 18:13:27 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972807@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28540] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 diff --git a/rt/t/data/emails/8859-15-message-series/msg6 b/rt/t/data/emails/8859-15-message-series/msg6 new file mode 100755 index 000000000..3ae9d3b69 --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/msg6 @@ -0,0 +1,35 @@ +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 29108 invoked by uid 9804); 26 May 2003 18:14:15 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:14:10 +0200 +Received: (Qmail 29066 invoked from network); 26 May 2003 18:14:10 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:10 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKbw-0000Zr-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:14:08 +0200 +Received: (qmail 29054 invoked by uid 9804); 26 May 2003 18:14:08 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:14:04 +0200 +Received: (Qmail 29036 invoked from network); 26 May 2003 18:14:04 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:04 +0200 +Date: Mon, 26 May 2003 18:14:24 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972864@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29069] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 diff --git a/rt/t/data/emails/8859-15-message-series/msg7 b/rt/t/data/emails/8859-15-message-series/msg7 new file mode 100755 index 000000000..6149dd644 --- /dev/null +++ b/rt/t/data/emails/8859-15-message-series/msg7 @@ -0,0 +1,36 @@ +Return-Path: <pape@inf.fu-berlin.de> +Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de +Received: (qmail 29551 invoked by uid 9804); 26 May 2003 18:15:16 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:15:12 +0200 +Received: (Qmail 29521 invoked from network); 26 May 2003 18:15:12 +0200 +Received: From es.inf.fu-berlin.de (160.45.110.22) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:12 +0200 +Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de) + by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian)) + id 19KKcx-0000a4-00 + for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:15:11 +0200 +Received: (qmail 29511 invoked by uid 9804); 26 May 2003 18:15:10 +0200 +Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1) + by localhost with SMTP; 26 May 2003 18:15:07 +0200 +Received: (Qmail 29465 invoked from network); 26 May 2003 18:15:06 +0200 +Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36) + by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:06 +0200 +Date: Mon, 26 May 2003 18:15:26 +0200 +From: Dirk Pape <pape@inf.fu-berlin.de> +To: staff@tec.mi.fu-berlin.de +Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?= +Message-ID: <2147483647.1053972926@[10.0.255.35]> +X-Mailer: Mulberry/3.0.3 (Mac OS X) +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Envelope-Sender: pape@inf.fu-berlin.de +X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29524] (NAI-uvscan@math.fu-berlin.de) +X-Remote-IP: 160.45.110.22 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +test nochmal in anderer Queue +test =E4=F6=FC=DF=C4=D6=DC=DF=A4 + diff --git a/rt/t/data/emails/crashes-file-based-parser b/rt/t/data/emails/crashes-file-based-parser new file mode 100755 index 000000000..da1913eb9 --- /dev/null +++ b/rt/t/data/emails/crashes-file-based-parser @@ -0,0 +1,193 @@ +X-Real-To: <mitya@example.com> +Received: from [194.87.5.31] (HELO sinbin.d-s.example.com) + by cgp.example.com (CommuniGate Pro SMTP 4.0.6/D4) + with ESMTP-TLS id 125035761 for mitya@example.com; Thu, 11 Dec 2003 15:17:46 +0300 +Received: (from daemon@localhost) + by sinbin.d-s.example.com (8.12.9p1/8.11.6) id hBBCHjN0031595 + for mitya@example.com; Thu, 11 Dec 2003 15:17:45 +0300 (MSK) + (envelope-from noc@rt3.mx.example.com) +Received: from d-s.example.com by sinbin.d-s.example.com with ESMTP id hBBCHjar031575; + (8.12.9p2/D) Thu, 11 Dec 2003 15:17:45 +0300 (MSK) +X-Real-To: <mitya@example.com> +Sender: <noc@rt3.mx.example.com> (Network Operation Center) +To: mitya@example.com +Date: Thu, 11 Dec 2003 15:17:45 +0300 +Message-ID: <redirect-137509289@d-s.example.com> +X-Original-Return-Path: <vox19@b92.d-s.example.com> +Received: from [194.87.0.16] (HELO mail.d-s.example.com) + by d-s.example.com (CommuniGate Pro SMTP 4.1.5/D1) + with ESMTP id 120757484 for noc@rt3.mx.example.com; Mon, 27 Oct 2003 09:40:53 +0300 +Received: from [194.87.0.22] (HELO moscvax.d-s.example.com) + by mail.d-s.example.com (CommuniGate Pro SMTP 4.1.5/D) + with ESMTP-TLS id 107945800 for noc@rt3.mx.example.com; Mon, 27 Oct 2003 09:40:53 +0300 +Received: from d-s.example.com (mx.d-s.example.com [194.87.0.32]) + by moscvax.d-s.example.com (8.12.9/8.12.9) with ESMTP id h9R6erFm062621 + for <security@d.example.com>; Mon, 27 Oct 2003 09:40:53 +0300 (MSK) + (envelope-from vox19@b92.d-s.example.com) +Received: by d-s.example.com (CommuniGate Pro PIPE 4.1.5/D1) + with PIPE id 120757490; Mon, 27 Oct 2003 09:40:53 +0300 +Received: from [194.87.2.108] (HELO b92.d-s.example.com) + by d-s.example.com (CommuniGate Pro SMTP 4.1.5/D1) + with ESMTP-TLS id 120757480 for security@d.example.com; Mon, 27 Oct 2003 09:40:52 +0300 +Received: from b92.d-s.example.com (localhost [127.0.0.1]) + by b92.d-s.example.com (8.12.8p1/8.12.3) with ESMTP id h9R6eqIe014669 + for <security@d.example.com>; Mon, 27 Oct 2003 09:40:52 +0300 (MSK) + (envelope-from vox19@b92.d-s.example.com) +Received: from localhost (localhost [[UNIX: localhost]]) + by b92.d-s.example.com (8.12.8p1/8.12.3/Submit) id h9R6epst014668 + for security@d.example.com; Mon, 27 Oct 2003 09:40:51 +0300 (MSK) +From: "Stanislav" <drstas@d.example.com> +Subject: Fwd: scanning my ports +X-Original-Date: Mon, 27 Oct 2003 10:40:51 +0400 +User-Agent: KMail/1.5.4 +X-Original-To: security@d.example.com +MIME-Version: 1.0 +Content-Type: Multipart/Mixed; + boundary="Boundary-00=_z3Ln/tUeUBipHgx" +X-Original-Message-Id: <200310270940.51758.vox19@d.example.com> +X-Spam-Checker-Version: SpamAssassin 2.60-jumbo.demos (1.212-2003-09-23-exp) +X-Spam-Level: +X-Spam-Status: No, hits=-6.8 required=5.0 tests=BAYES_00,FROM_ENDS_IN_NUMS, + HTML_MESSAGE,SUBJECT_RT autolearn=ham version=2.60-jumbo.demos +X-Spam-Report: -6.8 points, 5.0 required; + * -3.0 SUBJECT_RT Tracking system + * 1.0 FROM_ENDS_IN_NUMS From: ends in numbers + * 0.1 HTML_MESSAGE BODY: HTML included in message + * -4.9 BAYES_00 BODY: Bayesian spam probability is 0 to 1% + * [score: 0.0000] + + +--Boundary-00=_z3Ln/tUeUBipHgx +Content-Type: text/plain; + charset="koi8-r" +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + + +FYI + + +---------- Forwarded Message ---------- + +Subject: [DEMOS #12148] scanning my ports +Date: Sunday 26 October 2003 20:19 +From: 1stwizard@isp.example.com +To: no-reply@d-r.example.com + +This transaction appears to have no content + +------------------------------------------------------- + + + +-- +best wishes, + +Stanislav A. Mushkat +http://www.di.example.com + +--Boundary-00=_z3Ln/tUeUBipHgx +Content-Type: text/plain; + charset="iso-8859-1"; + name=" " +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Somebody at IP 127.0.0.1 scanned my ports. +--Boundary-00=_z3Ln/tUeUBipHgx +Content-Type: text/html; + charset="iso-8859-1"; + name=" " +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +<HTML><HEAD> +<META http-equiv=Content-Type content="text/html; charset=iso-8859-1"> +<META content="IncrediMail 1.0" name=GENERATOR> +<!--IncrdiXMLRemarkStart> +<IncrdiX-Info> +<X-FID>BA285063-5BCE-11D4-AF8D-0050DAC67E11</X-FID> +<X-FVER>2.0</X-FVER> +<X-FIT>Letter</X-FIT> +<X-FCOL>Elegant Paper</X-FCOL> +<X-FCAT>Stationery</X-FCAT> +<X-FDIS>Rice Fields</X-FDIS> +<X-Extensions>SU1CTDEsNDEsgUmBSTAkkcGNgZmVTY0wNCxNhYUoiU0kOMEoTYGBjYEoJDSZnSyFhUksSU1CTDIsMCwsSU1CTDMsMCwsVHlwZVZlcnNpb24sMywxLjAs</X-Extensions> +<X-BG>8E549F43-079D-11D8-B0F9-00B0D0B65B96</X-BG> +<X-BGT>repeat</X-BGT> +<X-BGC>#eff3f7</X-BGC> +<X-BGPX>left</X-BGPX> +<X-BGPY>0px</X-BGPY> +<X-ASN>ANIM3D00-NONE-0000-0000-000000000000</X-ASN> +<X-ASNF>0</X-ASNF> +<X-ASH>ANIM3D00-NONE-0000-0000-000000000000</X-ASH> +<X-ASHF>1</X-ASHF> +<X-AN>6486DDE0-3EFD-11D4-BA3D-0050DAC68030</X-AN> +<X-ANF>0</X-ANF> +<X-AP>6486DDE0-3EFD-11D4-BA3D-0050DAC68030</X-AP> +<X-APF>1</X-APF> +<X-AD>C3C52140-4147-11D4-BA3D-0050DAC68030</X-AD> +<X-ADF>0</X-ADF> +<X-AUTO>X-ASN,X-ASH,X-AN,X-AP,X-AD</X-AUTO> +<X-CNT>;</X-CNT> +</IncrdiX-Info> +<IncrdiXMLRemarkEnd--> +</HEAD> +<BODY style="BACKGROUND-POSITION: left 0px; FONT-SIZE: 12pt; MARGIN: 0px 10px 10px; COLOR: #00005b; BACKGROUND-REPEAT: repeat; FONT-FAMILY: Arial" text=#00005b bgColor=#eff3f7 background=cid:8E549F43-079D-11D8-B0F9-00B0D0B65B96 scroll=yes SIGCOLOR="0" X-ADF="0" X-AD="C3C52140-4147-11D4-BA3D-0050DAC68030" X-APF="1" X-AP="6486DDE0-3EFD-11D4-BA3D-0050DAC68030" X-ANF="0" X-AN="6486DDE0-3EFD-11D4-BA3D-0050DAC68030" X-ASHF="1" X-ASH="ANIM3D00-NONE-0000-0000-000000000000" X-ASNF="0" X-ASN="ANIM3D00-NONE-0000-0000-000000000000" X-FVER="2.0" X-FID="BA285063-5BCE-11D4-AF8D-0050DAC67E11" X-FIT="Letter" X-FCOL="Elegant Paper" X-FCAT="Elegant Paper" X-FDIS="Rice Fields" ORGYPOS="0"> +<TABLE id=INCREDIMAINTABLE cellSpacing=0 cellPadding=2 width="100%" border=0> +<TBODY> +<TR> +<TD id=INCREDITEXTREGION style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-SIZE: 12pt; PADDING-BOTTOM: 0px; CURSOR: auto; PADDING-TOP: 0px" vAlign=top width="100%"> +<DIV>Somebody at IP 127.0.0.1 scanned my ports. </DIV> +<DIV> </DIV> +<DIV> </DIV></TD></TR> +<TR> +<TD id=INCREDIFOOTER width="100%"> +<TABLE cellSpacing=0 cellPadding=0 width="100%"> +<TBODY> +<TR> +<TD width="100%"></TD> +<TD id=INCREDISOUND vAlign=bottom align=middle></TD> +<TD id=INCREDIANIM vAlign=bottom align=middle></TD></TR></TBODY></TABLE></TD></TR></TBODY></TABLE></BODY></HTML> +--Boundary-00=_z3Ln/tUeUBipHgx +Content-Type: image/jpeg; + charset="iso-8859-1"; + name="BackGrnd.jpg" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="BackGrnd.jpg" + +/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAHgAA/+4AIUFk +b2JlAGTAAAAAAQMAEAMCAwYAAAHbAAAC1gAABZX/2wCEABALCwsMCxAMDBAX +Dw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoXHh4jJSclIx4vLzMzLy9AQEBAQEBA +QEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoaJjAjHh4eHiMw +Ky4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIAGUAcwMBIgACEQED +EQH/xACAAAEBAQEAAAAAAAAAAAAAAAAAAQIGAQEBAAAAAAAAAAAAAAAAAAAA +ARABAAICAwEAAgMAAAAAAAAAAQARIQIxQRIiQDIQMFARAAICAgIBBAIDAQEA +AAAAAAERACExQVFhcYGRobECEsHhMtHxEgEAAAAAAAAAAAAAAAAAAABQ/9oA +DAMBAAIRAxEAAADtRZYE1ASghQFgUZoCkKSwLmhcllAEqkSkqFAlhUomoAS3 +IoJqFlDNpFEAQFE1AIVYAWIVKAJRNZpYCwVmmshKACA0CBAUCBYGwf/aAAgB +AgABBQD8B/yP/9oACAEDAAEFAPz6/or8H//aAAgBAQABBQC2+ZeHjbD+saX6 +hwXeDW1Rg4xLLTa+m7ZiIEsI1MTiHP1dYpvFADiFM1/X6nq9byuwdPPz5oFo +fWlEMQ9ULKrWq2ppG9Y2J6INQma9lVTRdlUKgHzXXSEECw1SYu5WsGoJPkis +ZYpx31GvXZQ/JM3VwShzVTsp1EZbBI8LcaUSih86+s2Zl4Wp6+lAZnVsDkjd +ku5m+lJTdXDG2SHM9M2wKX1YxsaZTTwmoVrYnqsMrM652yjs01K0mtbGAz6Y +5dpfqNz06qpq5QNjiIjiZtbhtceNuf0jyeqGgu6rXMvI4omPWbPMYzEfMI+a +xHnFvOP4/9oACAECAgY/AGP/2gAIAQMCBj8AY//aAAgBAQEGPwB72Yucb1Bf +IhFEaeZ+xRXFQELN+HEUQdjU0Xn4g9gRCQcpw1yajGYsP/kFvUzvjUBWrIMF +HI2OJQNEAjiEEFdTmfG/MTHq5RFOnpTV3kzCBx7x4YOD1AV5uYJvnqMA0hep +jfwpYCwC4Bx3q55zeZRBCw9TkoIuHw78RdczSNH2mgqcLpRC+RASAkA3B13m +cYd5mR84c/yOx4lWtRAZ6mGDhiP9WgXVyhWA+xDgMOWGMsTg/wBTz8SjjXrP +8hHIlX1MZ6mDzgc/cIV/iyN1GBR0MQMKjnEzvvMz8mUkErKlfqU63iV+IKNH +7mNZBLFQEpEDeDOV32IVn8WR4caoywqI2p695mbZzNUQIcKfk0bo+0NpCqn7 +CiQiNGXkdQen1DpjGeZ7WNw3pK+I93maCPc16+Zkf6XxMCsFwAkaiIB57vc/ +IAhZ/HqZBBbB0ZokAEOGxsYqBgPp8agQBu4VSMJdqx6SwDsGBrTmAR93uZGX +6KePowEADAIjoX8gw459CICaW/MLGvodQfkDW71zBxRHtB3j3jC4PMIYoAgK +NfPMCQNN7jCzvlzXPopzhQvNZY3CRya9ZrEFfRE0iCB5mscZuVYfKmAi94uE +3Q8qfytQ7xD0svmFcmaxNPI8iMjh3pmF2HbzqeUi+YkiD/MrOl5LmbwPuWVf +mXpv3hDH8qAjPpiZHXkRnSd6ZhB53mejzKV6US0K9TCCLyCeIhtETX5MsHBG +JkD/ANiFkMCE2qGoCdZ8Q8AMGpYFqEhdhRIYH3CF3d1M/Mexma+4CwdQ2Ddc +x0exAlmj04QUQd8QWLB/iB5GxmEg5TENVZqPYzFV8eHAy9T/AEc8a4n3Ov6g +/VwvE6lpQ4VNysXzhS8esOO8w/rlF/rypjV3B5H1Knr8T//Z + +--Boundary-00=_z3Ln/tUeUBipHgx-- + diff --git a/rt/t/data/emails/lorem-ipsum b/rt/t/data/emails/lorem-ipsum new file mode 100644 index 000000000..1aceb1464 --- /dev/null +++ b/rt/t/data/emails/lorem-ipsum @@ -0,0 +1,5 @@ +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut +labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris +nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit +esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +culpa qui officia deserunt mollit anim id est laborum. diff --git a/rt/t/data/emails/multipart-alternative-with-umlaut b/rt/t/data/emails/multipart-alternative-with-umlaut new file mode 100755 index 000000000..1ad4fe323 --- /dev/null +++ b/rt/t/data/emails/multipart-alternative-with-umlaut @@ -0,0 +1,62 @@ +Return-Path: <gst@example.com> +Delivered-To: j@pallas.eruditorum.org +Received: from vis.example.com (vis.example.com [212.68.68.251]) + by pallas.eruditorum.org (Postfix) with SMTP id 59236111C3 + for <jesse@example.com>; Thu, 12 Jun 2003 02:14:44 -0400 (EDT) +Received: (qmail 29541 invoked by uid 502); 12 Jun 2003 06:14:42 -0000 +Received: from sivd.example.com (HELO example.com) (192.168.42.1) + by 192.168.42.42 with SMTP; 12 Jun 2003 06:14:42 -0000 +Received: received from 172.20.72.174 by odie.example.com; Thu, 12 Jun 2003 08:14:27 +0200 +Received: by mailserver.example.com with Internet Mail Service (5.5.2653.19) id <LJSB7T54>; Thu, 12 Jun 2003 08:14:39 +0200 +Message-ID: <50362EC956CBD411A339009027F6257E013DD495@mailserver.example.com> +Date: Thu, 12 Jun 2003 08:14:39 +0200 +From: "Stever, Gregor" <gst@example.com> +MIME-Version: 1.0 +X-Mailer: Internet Mail Service (5.5.2653.19) +To: "'jesse@example.com'" <jesse@example.com> +Subject: RE: [rt-users] HTML-encoded mails with umlaute +Date: Thu, 12 Jun 2003 08:14:39 +0200 +Content-Type: multipart/alternative; + boundary="----_=_NextPart_001_01C330A9.E7BDD590" +X-Spam-Status: No, hits=0.0 required=5.0 + tests=AWL,HTML_50_60,HTML_MESSAGE,INVALID_DATE + version=2.55 +X-Spam-Level: +X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp) + +------_=_NextPart_001_01C330A9.E7BDD590 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Hello, + +ist this kind of Messages, that causes rt to crash.=20 + +Mit freundlichen Gr=FC=DFen +Gregor Stever ^^causes Error!! + + +------_=_NextPart_001_01C330A9.E7BDD590 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<HTML><HEAD> +<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; charset=3Diso-8859-= +1"> + + +<META content=3D"MSHTML 6.00.2800.1170" name=3DGENERATOR></HEAD> +<BODY> +<DIV><FONT face=3DArial><FONT size=3D2>Hello,<BR><BR>ist this kind of Messa= +ges, that=20 +causes rt to crash.<BR><BR>Mit freundlichen Gr=FC=DFen<BR>Gregor=20 +Stever ^^causes Error<SPAN=20 +class=3D975501206-12062003>!!</SPAN></FONT></FONT></DIV></BODY></HTML> + + +------_=_NextPart_001_01C330A9.E7BDD590-- + + diff --git a/rt/t/data/emails/multipart-report b/rt/t/data/emails/multipart-report new file mode 100755 index 000000000..538e0c880 --- /dev/null +++ b/rt/t/data/emails/multipart-report @@ -0,0 +1,66 @@ +Return-Path: <mailnull@example.com> +Date: Sat, 23 Aug 2003 00:15:18 +0800 (SGT) +From: Mail Delivery Subsystem <MAILER-DAEMON@other.example.com> +Message-Id: <200308221615.CGA36111@mailbox.other.example.com> +To: support@example.com +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="CGA36111.1061568918/mailbox.other.example.com" +Subject: Returned mail: User unknown +Auto-Submitted: auto-generated (failure) + +This is a MIME-encapsulated message + +--CGA36111.1061568918/mailbox.other.example.com + +The original message was received at Sat, 23 Aug 2003 00:15:18 +0800 (SGT) +from mx12.mcis.other.example.com [10.1.1.232] + + ----- The following addresses had permanent delivery errors ----- +<jesmund> + + +--CGA36111.1061568918/mailbox.other.example.com +Content-Type: message/delivery-status + +Reporting-MTA: dns; mailbox.other.example.com +Arrival-Date: Sat, 23 Aug 2003 00:15:18 +0800 (SGT) + +Final-Recipient: RFC822; jesmund@mailbox.other.example.com +Action: failed +Status: 5.1.1 +Remote-MTA: DNS; mail.mcis.other.example.com +Diagnostic-Code: SMTP; 550 5.1.1 <jesmund>... User unknown +Last-Attempt-Date: Sat, 23 Aug 2003 00:15:18 +0800 (SGT) + +--CGA36111.1061568918/mailbox.other.example.com +Content-Type: message/rfc822 + +Return-Path: <support@example.com> +Received: from mx12.other.example.com (mx12.mcis.other.example.com [10.1.1.232]) + by mailbox.other.example.com (Mirapoint Messaging Server MOS 3.3.3-GR) + with ESMTP id CGA36101; + Sat, 23 Aug 2003 00:15:17 +0800 (SGT) +Received: from STATION13 (rhala.dsl.pe.net [64.38.69.104]) + by mx12.other.example.com (8.12.9/8.12.9) with ESMTP id h7MGFGac020135 + for <jesmund@other.example.com>; Sat, 23 Aug 2003 00:15:17 +0800 +Message-Id: <200308221615.h7MGFGac020135@mx12.other.example.com> +From: <support@example.com> +To: <jesmund@other.example.com> +Subject: Thank you! +Date: Fri, 22 Aug 2003 9:15:19 --0700 +X-MailScanner: Found to be clean +Importance: Normal +X-Mailer: Microsoft Outlook Express 6.00.2600.0000 +X-MSMail-Priority: Normal +X-Priority: 3 (Normal) +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="_NextPart_000_05684DA4" + + + +--_NextPart_000_05684DA4-- + +--CGA36111.1061568918/mailbox.other.example.com-- + diff --git a/rt/t/data/emails/nested-mime-sample b/rt/t/data/emails/nested-mime-sample new file mode 100755 index 000000000..8b85d948c --- /dev/null +++ b/rt/t/data/emails/nested-mime-sample @@ -0,0 +1,396 @@ +Return-Path: <Xxxxxx_Yyyyyyy@some.net> +Delivered-To: jesse@pallas.eruditorum.org +Received: by pallas.eruditorum.org (Postfix) + id B5D3E1123A; Fri, 12 Jul 2002 11:35:27 -0400 (EDT) +Delivered-To: rt-2.0-bugs@pallas.eruditorum.org +Received: from postman.some.net (postman.some.net [193.0.0.199]) + by pallas.eruditorum.org (Postfix) with SMTP id 2736011234 + for <rt-2.0-bugs@fsck.com>; Fri, 12 Jul 2002 11:35:27 -0400 (EDT) +Received: (qmail 11615 invoked by uid 0); 12 Jul 2002 15:35:26 -0000 +Received: from x22.some.net (HELO x22.some.net.some.net) (193.0.1.22) + by postman.some.net with SMTP; 12 Jul 2002 15:35:26 -0000 +Date: Fri, 12 Jul 2002 17:35:26 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +To: rt-0.0-bugs@fsck.com +Subject: Example MIME within MIME within MIME message +Message-ID: <Pine.LNX.4.44.0207121734250.25020-120000@x22.some.net> +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-192303556-1026488126=:25020" +X-Spam-Status: No, hits=4.0 required=7.0 + tests=DOUBLE_CAPSWORD,MIME_NULL_BLOCK,MIME_MISSING_BOUNDARY + version=2.31 +Content-Length: 11478 + + This message is in MIME format. The first part should be readable text, + while the remaining parts are likely unreadable without MIME-aware tools. + Send mail to mime@docserver.cac.washington.edu for more info. + +--12654081-192303556-1026488126=:25020 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +MIME is fun at times. + + +-- + Xxxxxx Yyyyyyy SOME + Systems/Network Engineer NCC + www.some.net - PGP000C8B1B Operations/Security + +--12654081-192303556-1026488126=:25020 +Content-Type: MULTIPART/Digest; BOUNDARY="12654081-2102091261-1026488126=:25020" +Content-ID: <Pine.LNX.4.44.0207121734322.25020@x22.some.net> +Content-Description: Digest of 2 messages + + This message is in MIME format. The first part should be readable text, + while the remaining parts are likely unreadable without MIME-aware tools. + Send mail to mime@docserver.cac.washington.edu for more info. + +--12654081-2102091261-1026488126=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121734320.25020@x22.some.net> +Content-Description: first outer message (fwd) + +Date: Fri, 12 Jul 2002 17:32:37 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: first outer message +Message-ID: <Pine.LNX.4.44.0207121732180.25020-120000@x22.some.net> +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-113777422-1026487957=:25020" + + +--12654081-113777422-1026487957=:25020 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +first outer message + +--12654081-113777422-1026487957=:25020 +Content-Type: MULTIPART/Digest; BOUNDARY="12654081-387266385-1026487957=:25020" +Content-ID: <Pine.LNX.4.44.0207121732222.25020@x22.some.net> +Content-Description: Digest of 2 messages + +--12654081-387266385-1026487957=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121732220.25020@x22.some.net> +Content-Description: middle message (fwd) + +Date: Fri, 12 Jul 2002 17:31:45 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: middle message +Message-ID: <Pine.LNX.4.44.0207121731190.25020-120000@x22.some.net> +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1711788944-1026487905=:25020" + + +--12654081-1711788944-1026487905=:25020 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +This is the first middle message + + +--12654081-1711788944-1026487905=:25020 +Content-Type: MULTIPART/Digest; BOUNDARY="12654081-1221085552-1026487905=:25020" +Content-ID: <Pine.LNX.4.44.0207121731262.25020@x22.some.net> +Content-Description: Digest of 2 messages + +--12654081-1221085552-1026487905=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731260.25020@x22.some.net> +Content-Description: This is the inner-most message (fwd) + +Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: This is the inner-most message +Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +inner-msg + + + +--12654081-1221085552-1026487905=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731261.25020@x22.some.net> +Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd) + +Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: another inner message (need two before pine will do the mime-digest + thing) +Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +again + + + +--12654081-1221085552-1026487905=:25020-- +--12654081-1711788944-1026487905=:25020-- + +--12654081-387266385-1026487957=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121732221.25020@x22.some.net> +Content-Description: middle message (fwd) + +Date: Fri, 12 Jul 2002 17:32:05 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: middle message +Message-ID: <Pine.LNX.4.44.0207121731470.25020-120000@x22.some.net> +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1731270459-1026487925=:25020" + + +--12654081-1731270459-1026487925=:25020 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +This is the second middle message + + +--12654081-1731270459-1026487925=:25020 +Content-Type: MULTIPART/Digest; BOUNDARY="12654081-128832654-1026487925=:25020" +Content-ID: <Pine.LNX.4.44.0207121731502.25020@x22.some.net> +Content-Description: Digest of 2 messages + +--12654081-128832654-1026487925=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731500.25020@x22.some.net> +Content-Description: This is the inner-most message (fwd) + +Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: This is the inner-most message +Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +inner-msg + + + +--12654081-128832654-1026487925=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731501.25020@x22.some.net> +Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd) + +Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: another inner message (need two before pine will do the mime-digest + thing) +Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +again + + + +--12654081-128832654-1026487925=:25020-- +--12654081-1731270459-1026487925=:25020-- + +--12654081-387266385-1026487957=:25020-- +--12654081-113777422-1026487957=:25020-- + +--12654081-2102091261-1026488126=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121734321.25020@x22.some.net> +Content-Description: 2nd outer message (fwd) + +Date: Fri, 12 Jul 2002 17:32:54 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: 2nd outer message +Message-ID: <Pine.LNX.4.44.0207121732380.25020-120000@x22.some.net> +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1955637437-1026487974=:25020" + + +--12654081-1955637437-1026487974=:25020 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +2nd outer message + + +--12654081-1955637437-1026487974=:25020 +Content-Type: MULTIPART/Digest; BOUNDARY="12654081-362457126-1026487974=:25020" +Content-ID: <Pine.LNX.4.44.0207121732412.25020@x22.some.net> +Content-Description: Digest of 2 messages + +--12654081-362457126-1026487974=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121732410.25020@x22.some.net> +Content-Description: middle message (fwd) + +Date: Fri, 12 Jul 2002 17:31:45 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: middle message +Message-ID: <Pine.LNX.4.44.0207121731190.25020-120000@x22.some.net> +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1711788944-1026487905=:25020" + + +--12654081-1711788944-1026487905=:25020 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +This is the first middle message + + +--12654081-1711788944-1026487905=:25020 +Content-Type: MULTIPART/Digest; BOUNDARY="12654081-1221085552-1026487905=:25020" +Content-ID: <Pine.LNX.4.44.0207121731262.25020@x22.some.net> +Content-Description: Digest of 2 messages + +--12654081-1221085552-1026487905=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731260.25020@x22.some.net> +Content-Description: This is the inner-most message (fwd) + +Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: This is the inner-most message +Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +inner-msg + + + +--12654081-1221085552-1026487905=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731261.25020@x22.some.net> +Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd) + +Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: another inner message (need two before pine will do the mime-digest + thing) +Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +again + + + +--12654081-1221085552-1026487905=:25020-- +--12654081-1711788944-1026487905=:25020-- + +--12654081-362457126-1026487974=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121732411.25020@x22.some.net> +Content-Description: middle message (fwd) + +Date: Fri, 12 Jul 2002 17:32:05 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: middle message +Message-ID: <Pine.LNX.4.44.0207121731470.25020-120000@x22.some.net> +MIME-Version: 1.0 +Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1731270459-1026487925=:25020" + + +--12654081-1731270459-1026487925=:25020 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +This is the second middle message + + +--12654081-1731270459-1026487925=:25020 +Content-Type: MULTIPART/Digest; BOUNDARY="12654081-128832654-1026487925=:25020" +Content-ID: <Pine.LNX.4.44.0207121731502.25020@x22.some.net> +Content-Description: Digest of 2 messages + +--12654081-128832654-1026487925=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731500.25020@x22.some.net> +Content-Description: This is the inner-most message (fwd) + +Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: This is the inner-most message +Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +inner-msg + + + +--12654081-128832654-1026487925=:25020 +Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII +Content-ID: <Pine.LNX.4.44.0207121731501.25020@x22.some.net> +Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd) + +Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST) +From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net> +X-X-Sender: bc@x22.some.net +To: Xxxxxx_Yyyyyyy@some.net +Subject: another inner message (need two before pine will do the mime-digest + thing) +Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net> +MIME-Version: 1.0 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + + +again + + + +--12654081-128832654-1026487925=:25020-- +--12654081-1731270459-1026487925=:25020-- + +--12654081-362457126-1026487974=:25020-- +--12654081-1955637437-1026487974=:25020-- + +--12654081-2102091261-1026488126=:25020-- +--12654081-192303556-1026488126=:25020-- + diff --git a/rt/t/data/emails/nested-rfc-822 b/rt/t/data/emails/nested-rfc-822 new file mode 100755 index 000000000..d4f118df2 --- /dev/null +++ b/rt/t/data/emails/nested-rfc-822 @@ -0,0 +1,253 @@ +Return-Path: <jonas@astral.example.com> +Delivered-To: j@pallas.eruditorum.org +Received: from example.com (example.com [213.88.137.35]) + by pallas.eruditorum.org (Postfix) with ESMTP id 869591115E + for <jesse@bestpractical.com>; Sun, 29 Jun 2003 18:04:04 -0400 (EDT) +Received: from jonas by example.com with local (Exim 4.20) + id 19WkLK-0004Vr-0I + for jesse@bestpractical.com; Mon, 30 Jun 2003 00:08:18 +0200 +Resent-To: jesse@bestpractical.com +Resent-From: Jonas Liljegren <jonas@example.com> +Resent-Date: Mon, 30 Jun 2003 00:08:17 +0200 +Received: from mail by example.com with spam-scanned (Exim 4.20) + id 19Wayz-00068j-KO + for jonas@astral.example.com; Sun, 29 Jun 2003 14:08:42 +0200 +Received: from jonas by example.com with local (Exim 4.20) + id 19Wayz-00068g-FY + for red@example.com; Sun, 29 Jun 2003 14:08:37 +0200 +To: Redaktionen <red@example.com> +Subject: [Jonas Liljegren] Re: [Para] =?iso-8859-1?q?Niv=E5er=3F?= +From: Jonas Liljegren <jonas@example.com> +Date: Sun, 29 Jun 2003 14:08:37 +0200 +Message-ID: <87d6gxt7ay.fsf@example.com> +User-Agent: Gnus/5.1002 (Gnus v5.10.2) Emacs/21.2 (gnu/linux) +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" +Sender: Jonas Liljegren <jonas@astral.example.com> +Resent-Message-Id: <E19WkLK-0004Vr-0I@example.com> +Resent-Sender: Jonas Liljegren <jonas@astral.example.com> +Resent-Date: Mon, 30 Jun 2003 00:08:18 +0200 +X-Spam-Status: No, hits=-5.7 required=5.0 + tests=AWL,BAYES_10,EMAIL_ATTRIBUTION,MAILTO_WITH_SUBJ, + QUOTED_EMAIL_TEXT,USER_AGENT_GNUS_UA + version=2.55 +X-Spam-Level: +X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp) + +--=-=-= +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Material f=F6r att uppdatera texten om niv=E5er. + +Denna text b=F6r dessutom ligga som ett uppslagsord och inte d=E4r den ligg= +er nu. + + +--=-=-= +Content-Type: message/rfc822 +Content-Disposition: inline + +Return-path: <list-bounces@example.com> +Received: from mail by example.com with spam-scanned (Exim 4.20) + id 19WFzq-0005i1-WE + for jonas@example.com; Sat, 28 Jun 2003 15:44:13 +0200 +Received: from localhost + ([127.0.0.1] helo=example.com ident=list) + by example.com with esmtp (Exim 4.20) + id 19WFzp-0005hf-Tz; Sat, 28 Jun 2003 15:44:05 +0200 +Received: from mail by example.com with spam-scanned (Exim 4.20) + id 19WFzh-0005hR-Bu + for list@example.com; Sat, 28 Jun 2003 15:44:03 +0200 +Received: from jonas by example.com with local (Exim 4.20) + id 19WFzh-0005hO-AO + for list@example.com; Sat, 28 Jun 2003 15:43:57 +0200 +To: list@example.com +Subject: Re: [Para] =?iso-8859-1?q?Niv=E5er=3F?= +References: <002701c33d62$170fb2e0$a33740d5@TELIA.COM> + <002301c33d66$bf6483e0$d97864d5@remotel2tu76c8> + <64753.217.210.4.156.1056801224.squirrel@example.com> +From: Jonas Liljegren <jonas@example.com> +Date: Sat, 28 Jun 2003 15:43:57 +0200 +In-Reply-To: <64753.217.210.4.156.1056801224.squirrel@example.com> (Jakob + Carlsson's message of "Sat, 28 Jun 2003 13:53:44 +0200 (CEST)") +Message-ID: <877k76uxk2.fsf@example.com> +User-Agent: Gnus/5.1002 (Gnus v5.10.2) Emacs/21.2 (gnu/linux) +X-BeenThere: list@example.com +X-Mailman-Version: 2.1.2 +Precedence: list +List-Id: Öppen lista för alla medlemmar <list.example.com> +List-Unsubscribe: <http://example.com/cgi-bin/mailman/listinfo/list>, + <mailto:list-request@example.com?subject=unsubscribe> +List-Archive: <http://example.com/pipermail/list> +List-Post: <mailto:list@example.com> +List-Help: <mailto:list-request@example.com?subject=help> +List-Subscribe: <http://example.com/cgi-bin/mailman/listinfo/list>, + <mailto:list-request@example.com?subject=subscribe> +Sender: list-bounces@example.com +Errors-To: list-bounces@example.com +X-Spam-Status: No, hits=-7.0 required=5.0 + tests=BAYES_00,EMAIL_ATTRIBUTION,IN_REP_TO,QUOTED_EMAIL_TEXT, + REFERENCES,REPLY_WITH_QUOTES,USER_AGENT_GNUS_UA + version=2.55 +X-Spam-Level: +X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp) +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +"Jakob Carlsson" <esrange@example.com> writes: + +>> Om du g=E5r in p=E5 Hemsidan och sen p=E5 Torget. +>> D=E4r ser du att det st=E5r ditt anv=E4ndarnamn och +>> bredvid det Niv=E5 5. +>> Klicka p=E5 niv=E5 5 s=E5 kommer du in p=E5 en sida som +>> f=F6rklarar allt om niv=E5systemet. +> +> Bra svar. Men jag k=E4nner f=F6r att ge en kort f=F6rklaring av niv=E5-sy= +stemet. + +Jag skulle kunna l=E4gga en massa tid p=E5 at skriva samma sak om och om +igen. Fliker in h=E4r f=F6r att s=E4ga detta =E4nnu en g=E5ng...: + + * Det =E4r jag som hittat p=E5 det h=E4r med niv=E5system + + * Det =E4r en stor skillnad p=E5 hur det =E4r t=E4nkt att vara och hur det= + =E4r + nu. Jag har stora planer och en massa id=E9er jag vill genomf=F6ra. + + * Niv=E5systemet =E4r en =E5terkoppling f=F6r vad man gjort f=F6r webbplat= +sen. + Som ett tack g=F6r hj=E4lpen. + + * Systemet finns som en inspiration f=F6r de som d=E5 k=E4nner f=F6r att g= +=F6ra + mer. Men jag vill inte att det ska ge en negativ influens f=F6r de + som inte gillar niv=E5er. Var och en ska kunna v=E4lja att ignorera + niv=E5n. Speciellt b=F6r de f=F6rst=E5 att det inte har att g=F6ra med + graden av andlig utveckling, esoteriska kunskaper eller n=E5got + s=E5dant. + + * Inspirationen till niv=E5erna kommer fr=E5n spel, hemliga ordenssystem, + kosmska hiearkier, skr=E5v=E4sen, akademier, politisk administration, + osv. Det =E4r ett element av rollspel. En lek. + + * Olika niv=E5er motsvarar olika roller p=E5 webbplatsen. Webbplatsen + webbmaster och ansvbariga har en viss niv=E5, bes=F6kare och g=E4ster har + en annan niv=E5. + + * Alla datorsystem har administrat=F6rssystem f=F6r dem som sk=F6ter + systemet. Jag har valt att arrangera dessa funktioner i en skala. + Niv=E5n anger hur mycket av systemet du har r=E4tt att administrera. + + * Att ha ett niv=E5system f=F6r access g=F6r att man kan g=F6ra som p=E5 f= +ilm; + att l=E5ta de med h=F6gre access komma =E5t mer information. De med + riktigt h=F6g niv=E5 kan n=E5 topphemlig information. P=E5 denna webbpl= +ats + kan varje anv=E4ndae v=E4lja att h=E5lla vissa personliga uppgifter. Har + du h=F6g niv=E5 har du rollen som anv=E4ndaradministrat=F6r och har + tillg=E5ng till dessa uppgifter. Just nu =E4r vi tre personer med + denna niv=E5n. + + * Niv=E5systemet =E4r ett m=E5tt p=E5 p=E5litlighet. Vi ger dig h=F6gre n= +iv=E5 n=E4r + vi litar p=E5 att du inte kommer att f=F6rst=F6ra f=F6r oss. F=F6r ju h= +=F6gre + niv=E5, desto l=E4ttare kan du sabbotera inneh=E5llet. + + * P=E5 en h=F6gre niv=E5 beh=F6vs det inte bara att vi litar p=E5 att du v= +ill + v=E4l. Du m=E5ste =E4ven ha ett gott omd=F6me, teknisk f=F6rst=E5else, + intresse och logiskt t=E4nkande. Utan detta =E4r det l=E4tt h=E4nt att = +du + f=F6rst=F6r saker av misstag. + + * Vi vill uppmuntra medlemmarna att g=F6ra det som =E4r bra f=F6r + webbplatsen. Tilldelandet av h=F6gre niv=E5 ska uppmuntra till att + g=F6ra det som =E4r bra. + + * F=F6r att minska missbruk av e-postadresser visar vi e-postadresser + bara f=F6r de med lite h=F6gre niv=E5. P=E5 s=E5 vis vill vi undvika att + n=E5gon g=E5r med som medlem bara f=F6r att samla e-postadresser f=F6r a= +tt + sedan g=F6ra reklamutskick. + + * Idag n=E5r du olika niv=E5er p=E5 detta vis: + + 0. Kom in p=E5 webbplatsen som g=E4st + + 1. V=E4lj anv=E4ndarnamn och ange e-postadress + + 2. Logga in med det l=F6senord som skickats till dig + + 3. Skrivit en presentation + + 5. Presentationen har godk=E4nts + + 6. Du har svarat p=E5 ett f=E5tal fr=E5gor om dina intressen + + 7. Du har svarat p=E5 en massa fr=E5gor om intressen och beskrivit dem + detaljerat + + 10. N=E5gon v=E4ktare tycker du f=F6rtj=E4nar h=F6gre niv=E5 f=F6r att du= + =E4r s=E5 + trevlig och engagerad i webbplatsen. + + 11. Du har gjort ett antal kopplingar mellan =E4mnen och =F6verv=E4gande + delan av dem har godk=E4nts av en v=E4ktare, och du accepterar att + b=F6rja som l=E4rling i v=E4ktarakademin (jobbet som + systemadministrat=F6r) + + 12-39. D=E5 och d=E5 tittar jag p=E5 vad du gjort i form av skrivande av + texter och arbetande med uppslagsverkets =E4mnen, och justerar din + niv=E5 i f=F6rh=E5llande till m=E4ngd och kvalit=E9 p=E5 arbetet + + 40. Du har full=E4ndat ett helt =E4mnesomr=E5de. En m=E4ngd sammanl=E4nk= +ade + =E4mnen med bra textinneh=E5ll. + + 41. F=F6rtroende att arbeta med adminstration av medlemsregistret. + + 42. Delaktig i utvecklandet av webbplatsens prgrammering. + + + * Allts=E5. Automatik tar dig till niv=E5 7. + + * Men som sagt. Jag har en massa andra planer d=E4r mycket mer kopplas + till niv=E5er och d=E4r det finns systemautomatik f=F6r hela v=E4gen till + niv=E5 40. + + * 41 och 42 ligger utanf=F6r niv=E5systemet i =F6vrigt. Den som har de + niv=E5erna har inte n=F6dv=E4ndigtvis tagit sig till niv=E5 40 innan. De + motsvaras av anv=E4ndaradministrat=F6r och systemadministrat=F6r och + niv=E5n speglar maktbefogenheterna snarare =E4n vad man i =F6vrigt gjort + f=F6r webbplatsen. + + * Alla texter. Allt inneh=E5ll =E4r =F6ppet f=F6r alla. =C4ven f=F6r bes= +=F6kare som + inte loggar in. Du kan till och med g=E5 in p=E5 + administrationssidorna utan att logga in. Vi g=F6mmer inte inneh=E5ll. + Det vi g=F6r =E4r att hindra dig fr=E5n att =E4ndra inneh=E5llet. Vi d= +=F6ljer + dock en del information om andra medlemmar i syfte att f=E5 s=E5 m=E5nga + som m=F6jligt att sj=E4lv skriva in sig och skriva en presentation. + +--=20 +/ Jonas - http://jonas.example.com/myself/en/index.html + +_______________________________________________ +List mailing list +List@example.com +http://example.com/cgi-bin/mailman/listinfo/list + + +--=-=-= + + + +-- +/ Jonas - http://jonas.example.com/myself/en/index.html + +--=-=-=-- + diff --git a/rt/t/data/emails/new-ticket-from-iso-8859-1 b/rt/t/data/emails/new-ticket-from-iso-8859-1 new file mode 100755 index 000000000..299392d26 --- /dev/null +++ b/rt/t/data/emails/new-ticket-from-iso-8859-1 @@ -0,0 +1,31 @@ +Return-Path: <hw@nordkapp.net> +Delivered-To: j@pallas.eruditorum.org +Received: from sm1.nordkapp.net (sm1.nordkapp.net [62.70.54.150]) + by pallas.eruditorum.org (Postfix) with ESMTP id 48F4E11112 + for <jesse@bestpractical.com>; Mon, 2 Jun 2003 14:58:37 -0400 (EDT) +Received: (qmail 3612 invoked by uid 1009); 2 Jun 2003 18:58:36 -0000 +Received: from unknown (HELO office.nordkapp.net) (213.161.186.83) + by 0 with SMTP; 2 Jun 2003 18:58:36 -0000 +Message-Id: <5.2.1.1.0.20030602205708.0314c5f8@mail.nordkapp.net> +X-Sender: hw@nordkapp.net@mail.nordkapp.net +X-Mailer: QUALCOMM Windows Eudora Version 5.2.1 +Date: Mon, 02 Jun 2003 20:58:30 +0200 +To: Jesse Vincent <jesse@bestpractical.com> +From: Wilhelmsen Håvard <hw@nordkapp.net> +Subject: Re: rt-3.0.3pre1 +In-Reply-To: <20030602185607.GN10811@fsck.com> +References: <5.2.1.1.0.20030602204834.031406d8@mail.nordkapp.net> + <5.2.1.1.0.20030530194214.0371c988@mail.nordkapp.net> + <5.2.1.1.0.20030530194214.0371c988@mail.nordkapp.net> + <5.2.1.1.0.20030602204834.031406d8@mail.nordkapp.net> +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1"; format=flowed +Content-Transfer-Encoding: 8bit +X-Spam-Status: No, hits=-1.9 required=5.0 + tests=AWL,EMAIL_ATTRIBUTION,IN_REP_TO,QUOTED_EMAIL_TEXT, + REFERENCES,REPLY_WITH_QUOTES + autolearn=ham version=2.55 +X-Spam-Level: +X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp) + +Håvard diff --git a/rt/t/data/emails/new-ticket-from-iso-8859-1-full b/rt/t/data/emails/new-ticket-from-iso-8859-1-full new file mode 100755 index 000000000..493ca1591 --- /dev/null +++ b/rt/t/data/emails/new-ticket-from-iso-8859-1-full @@ -0,0 +1,38 @@ +X-Mailer: QUALCOMM Windows Eudora Version 5.2.1 +To: Jesse Vincent <jesse@bestpractical.com> +From: Wilhelmsen Håvard <hw@nordkapp.net> +Subject: Re: rt-3.0.3pre1 +X-Spam-Status: No, hits=-1.9 required=5.0 + tests=AWL,EMAIL_ATTRIBUTION,IN_REP_TO,QUOTED_EMAIL_TEXT, + REFERENCES,REPLY_WITH_QUOTES + autolearn=ham version=2.55 +X-Spam-Level: +X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp) + +At 14:56 02.06.2003 -0400, you wrote: +>> This patch didn't help us out. +>> We still got problems with auto responding e-mails sent from the system +>> when a new ticket is created. +>> The same problem appears when one of the staff replays to an new ticket. +>> All Norwegian letters is converted to strange letters like ø +>> +>> We would love if this bug could be fixed. On our mail server we are +>running +>> perl 5.6.1 since we are using debian stabel packet lists. +> +>I'd love it too. I just can't find it. Can you send me +>(jesse@bestpractical.com) a couple of email messages containing +>characters that break your RT? + +Hello again, + +Thanks for your fast replay! + +I don't know how this looks at your end but it is letters like: ø æ Ã¥ +If your want to make this in html it will be ø å and &aerlig; + + +-- +HÃ¥vard + + diff --git a/rt/t/data/emails/notes-uuencoded b/rt/t/data/emails/notes-uuencoded new file mode 100755 index 000000000..f27fdf8c0 --- /dev/null +++ b/rt/t/data/emails/notes-uuencoded @@ -0,0 +1,2368 @@ +Return-Path: <mhenrion@example.com> +Delivered-To: j@pallas.eruditorum.org +Received: from serveurlotus.example.com (unknown [213.56.193.67]) + by pallas.eruditorum.org (Postfix) with SMTP id C21DB113AA + for <jesse@vendor.example.com>; Thu, 27 Nov 2003 10:55:58 -0500 (EST) +Received: by serveurlotus.example.com(Lotus SMTP MTA v4.6.1 (569.2 2-6-1998)) id C1256DEB.00578401 ; Thu, 27 Nov 2003 16:55:54 +0100 +X-Lotus-FromDomain: DOMAINEQZ +From: "Maxime HENRION" <mhenrion@example.com> +To: jesse@vendor.example.com +Cc: support@example.com +Message-ID: <C1256DEB.005717B5.00@serveurlotus.example.com> +Date: Thu, 27 Nov 2003 16:55:50 +0100 +Subject: Test e-mail which exhibits problems with RT +X-Spam-Status: No, hits=-2.6 required=7.0 + tests=BAYES_20 + version=2.55 +X-Spam-Level: +X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp) +Content-Length: 144905 + +I send you this mail from Lotus Notes to make sure it'll exhibit the +reported symptoms (lost attachment and body). I Cc: it to our RT address +to verify it does cause the reported problems. Could you please mail me +any replies to my personal e-mail, mux@example.org ? + +Thanks in advance, +Maxime + +(See attached file: Naz_Head.jpg) + +(UUEncoded file named: Naz_Head.jpg follows) +(Its format is: JPEG File Interchange ) + +begin 644 Naz_Head.jpg +M_]C_X``02D9)1@`!`@(```````#__@`>04-$(%-Y<W1E;7,@1&EG:71A;"!) +M;6%G:6YG`/_``!$(!(D#$P,!(@`"$0$#$0'_VP"$``0"`P,#`@0#`P,$!`0$ +M!@H&!@4%!@P("0<*#@P/#PX,#@T0$A<3$!$5$0T.%!L4%1<8&1H9#Q,<'AP9 +M'A<9&1@!!@8&"0<)$0D)$248%1@E)24E)24E)24E)24E)24E)24E)24E)24E +M)24E)24E)24E)24E)24E)24E)24E)24E)?_$`*(```$%`0$!`0`````````` +M``(``0,$!08'"`D0``$#`P,"!0($!`0$!0(""P$``A$#!"$2,4$%4083(F%Q +M@9$',J&Q%"-"P5+1X?`5,V+Q"!8D-%,7<H(U0QACDI.B)29&5%8!`0$!`0$! +M`0`````````````!`@,$!081`0$``@(#``$%`0$!`0$````!`A$A,0,205$$ +M$R(R87$%%5*!_]H`#`,!``(1`Q$`/P#Y`NZSM9#1`&(!5%[G\U"!WW5B](U: +M0(GDE5'DR3$]R3,K53ZB?5?D$I,JNC=`\EN#.=PHMVZH^%$6?,)R3(`R90/K +M:AB0!LHFG"&6Q`,?*(E\^"`"3.=TC6=J`)([!0M#=1U'X3.TZ9D[X**G=6=( +MR82\]X,ZC`&"H0X"9<3W2:(&GOWF4+VLMN7D2T[[AR*E5<YPU?>56:T,@P<X +M3^D#'*J+3:SC(<8[92\XB/5]U7VV$E/Z=(!.459_B7`0'&2F%<EX)D]R%7!) +M:$B1.<S^B(G-PYFQQ/*7G$P9/M!5>9,:ML)Y$]AW03FLXR78A.VY=&=IV58& +M2!^X1-BFWO)S""S_`!#M,.,'VV2;<.(C48"JR'#&!V*>!LUV_OLBK)KO@C5) +M_9)M=V0(GNJSAL9"7!!E066UR&X)^BD%P06DOR%2&00WG=$WYSQ[JHL&Y&>9 +MQV3ON7.;ZG:@.).%59,Y.#[)H&F7.`/QNBK;:Y#?S$XP"4(N'[:S[Y46H-8< +M2#C.4``SZA!V")I9?5=AVO[<IA7=J)C$[DJN""8!VY"3C$;@(6+?\4_83O@) +MZ=U4;@$YQNJ0<>')L[3G?=15_P#BG`0"[/=(W;N3NJ@/J]>8PG=M(<!P%46Q +M<5)C5!&Z%UR0^=6_,JJZ220[',XE!J(VD@HJZZZ<3J+H)X*=]T]K1ZLGW5*G +M+G3N$[-()F/_`,1E0TN-N7@RUY)[R<)V7=1V=1^ZH@.,F<CA$V/+F8CLJBW6 +MO'^8/4[[HA=U,^L@K/?#GB-N4Y)Y)CW15RI>5'#_`)CL>Z`7CY]1=`[$Y50& +M1,Y]BF&^G!]I07_XRII!#JD3]E(RO<NI.KM-30T@.=P"LV`T[C]D0<?+(DD$ +MR0D5<;?59(+S)P9)2_CJN_F.)VR51=+7@S'QA($N$#9$:-.^JR/63]5(R]K. +MP*CM1]UF'7L,=H1M=+IY0:#[ZL'R'N$#9/\`\0N`3-1Q!.8*HD..1N@]0)@@ +MQ[HK39U"J2!YA`/*3>HU9_/O]5G,+F"9@>Z?5D3Q[HC2'4*PF'9=]T_\?6!' +M\QQ^2LZG,[R-U(3Z):<<"9A*+K.I7(<2*KA]4XZI<:R[S78YE9H))$NB?=+4 +M(C.^4&O2ZM<:Y-=^F<PXJ9W6[ME1_E5ZA!V]1P%A%T-/'NI*+M(U3J/LLU8V +M[?KM\#_[NK,?XBI7>(+\`#^+JQ[/*P/,<'SI`]E/1D[@'.`D*V[GKMZUP>;N +MOW`#RJEQXCZB^KJ_BJ_P*CEG]1JD5PW5D?HJC2`V8,]P>$A.&PSKO41Z'7E; +M/_649\0]18YO_K:I@Y]9@K%:\DDM/P.4#G._J)39IT%3Q#U(48;>U9]GE*WZ +M_P!3:=;KVMCC65@L,M],_=3C\I9@^_*IIT-GX@ZL]P<;ZK#3)EY5YWB;J9`_ +M]75`]G9*YJB-%&!@G<=T?F!K`T3'LL4=(/$_5-`TWE4#_P"Y,?%750__`-[5 +MS[KG?,U-`)..W*$N$F=QW4TKH?\`S5U?7)O:@^J3O%/5&`3>5<_]2YQS_3,D +M90OJ%L9S[*CI'>*^JA\_QE4>VN?[)4?%76`''^/KCC\Q7-.?WG[PHR\9[\!" +M.I/C+K8R.I7(CL\J1OC?K[!J'5KH>WF$+DC5TM&#J_="YY=].$Z'8?\`G_Q( +MTS_QJ\$\"L[_`#1?_4;Q8T"/$-^('_SN_P`UQ.O/`(3O?Z<\JZAR[=WXG>+V +MMQXBZB)Y-R__`#3?_4[QHV2/$W4\<?Q3_P#-<,YTD-[(7N!.70!NAR[S_P"J +M'C(;>)NJ0.]W4_S3?_5'QII.KQ/U0@;?^I?M]UPDN()DCLE2J/%6>?8Y5X-U +MWP_$[QDTZO\`S+U0#L;A_P#FA/XI>-1__D_5!`__`-A_^:XBK4+A#=]LJ'62 +MT@@2H;KO:?XK^.`#'BCJ7_\`'=_FI&?B]XY;G_S1U$]OY[O\UYWYA+")*;6- +MNRJ/26_C)X]8UI_\T]1@XCSBI/\`ZT_B$W;Q5?D]O-*\R#P`#O&R$UC,SDH/ +M4*?XX?B,R)\6]1$?_M4=3\<OQ&F6^+>HB?\`]IM^B\LU.W(D$*.M7\L$-DGW +M31MZF_\`';\2*6H'Q3U!Q(P?.V_15S^./XE:-;O%O42WDFIM^B\K#_3).0>4 +M1>-,R,YB86M0Y>HL_'+\2"X$>*NH?_Q/]%+1_'7\1FNSXHO2>_F+RG5#,F/8 +M=U(S+@Z23\)J'+VNT_\`$'^)M.W:QOBJ[@=WJ3_]8;\3_P#_`*NZ_P#WUX]0 +M/\IOJ`13_P!8_P!_1.#E)>;X.DJE4`G4&CW]U>O/2YTY/95"VD\@O(8(_-NE +M-(*I$\#ZJ.((DP#MRC?I!,.#@3P@<YLQ@D*)T!Y=!!'V3,!(!DR-NZ/!DDS_ +M`)H=3FOU#\Q$3*`2[U;F3^J9FG5DG!VA%J&"\3VA,PC.H3]4#")SCZ[IPZ6Y +M.?;*9Q$P1">`3L`F@0!<W48QB)RDW\W8=DM)B0,C=(G'Y3OG*H)A]7YC]TGO +M`;J&VTI8`@$@QRF`F)A`6HZ<&,(`XM$-),HM.TR`DTC)_9`@]H'?NB(U9)D* +M)Q`R/L"G:]K3D$_*"3`)DS'(_P`X2>0`")).=10F`,SG9*`Y^DS'9#0VZM.' +M@0=NZ51S@XYR=PG9IF'?1!4<)QF4!`X&<E.9%3F2.Z$Z-(:!$Y30-4SL@D`] +M<ETSPD2`X%Q$#W3.>TTQ@",3R5'[%V$1,Y\B8S&Q0:S^;4")R9@(2^`<P>T[ +M)`@^IH&GL%%2-?@B8^J;60">3]T+=C.J1O@_ND<_U8'(RJ<E))W`GE,?=Q,< +M%+TDD@R?E-$C#A[A0$`79&/JF!<U^X^B'!YRDUOJ(*HF#AY@Q&$6KDNB%"0" +M9!2<8.3)Y4$CW2\MF#^Z8.(;!&KZIG-P-L#A"V6F=8!/""0%I.0/<(75#/:= +M@$.F:>8`/)3#3OJ'T*(D:[&8B=Y4C7$"&M,J)L1Q'>$3@1L8'L@8F3@1]4+G +M&(VX2(#3^:>\%`?54P9'SNJHZ;B.^4]0D@X!(0@..QCZIR`V"7-.)0)CR6P1 +MDHFZ@-_B<H`WU1IGV3L#L0!`X)*:41=.<GX1,?\`TYA``=<C`&0EI/,90&PN +M+IQMR437D#8PHVM)B'>VZ.>9&?T32';4C&4FN.24S!G,B.4@"7P-D4;CZ02G +MUQM@'E,`7,D-F-TP:03#B)XW03TW0`3/=.ZHT[20HJ0(=JP<83U=1'Y??=$I +M.>20)B/JF<\D^W<!#)DQB>$)+@3@D=@@.1J(C\RGI.TTAL`%!;@U'@&8)4U< +M#40..%*LA!WKD@?,JQ3<*;=6J?959U`<%*HXBG,R=MU`JM05+G42#[QLF>8: +M0V"1O)W4;):`V3G>$4@$&()W0/YA+H&$S"=Q$'E,X34D!.&MF<>_NBIZ.G5. +M1C=6*32XZSMP56:Z#I#1/=6ZKAY8:!$;PI:1(7B!)SV0O<T[&(0BF-&<#W05 +M7:G%L8&%!,UPC`$_*?7)))4+&NF9P-@0G/R,]D!$M)DGX"3G%P`W^NZ!A;OB +M1[R@JU"1F(;VP4!ET'TD3[("8?&,^R`O!&,&=@A($`P2.TJFAQZAK/V0U()] +M),\A(QITP/C>$+R(+B![(IY&DQN$!U$=OE*99G;V0ZVDSVXE7:0I$@DY12W5 +MF0.P47ID%(N@03]R@.1DL'ZH<X)#L\RF$;:O]$Y8WR]0J-+I_+&?^R&A.=`G +M'T0!\2[)*%P,1D`<(7N`@$_JB40>#_DD#^G"C!R=_NE()$'?!0$X%Q$?NF(& +MY!&.Z;;4`24#RYH!!)]T#U7A@,\JNZ-6?ND\EYDN$]T]-[\MUX[0M!/;+?E, +MUDQ/WW2=4TGL3V*!SAQQS*(D:WD$P?T4M+8@29W]E!J],CE'J&H`.XR$&G0< +M!1:(&R/6WL%#1TFDTEW'9%#/\?Z)N+I/<F"1,=]7^JI5G`N(';`[*[?$.<7[ +MS]BJ=8@'&$J('2&YF3QV0`-+`XD3VY1<D$@'=`T>L@B%`B9(SQF$/&TIHT[B +M2F;!.3'L4#N:&P[?Y*:1!WD=T\S(.3\RA,:2Z.?T3:GD%I&Y&/A*8&9CLA(S +M$B/S8PG<V88QIQGOE$T-CV#9SB>Q&R<Z09CW(40V@#9$=IP/>%0;B#F#CW14 +MXU>QX4?]1X*3,$X(]U!,_2'0XS"%S@'20(CLAJF'#E)I!!:2J$XMS(]L(26P +M)S[(W._EENH@3J4+FR2/902.TZCP(V[HH!!C(*A;#@)V[<J6F<R3J&,3NA3@ +MXW*8",@'(15&AQD8C.\H>8@M^"J#<1&QE-SL0.Y2(SOLE'IF<($XB<F/="8+ +M(<<3$I/TD@`SVE!I].))"@-S@1(=@I"=)SNA((80#*(/$`M:!`SE%%,`#7&$ +MCN"YT_51DNB`Z0#CA.0X_5(AQ&`23[E/L1W/"C@.).T9*3S+@1/9/\.AM!.2 +M0">$3P0[)"%VD9!D#<I./I[IL2'5$`R$!&&SQA"UQTF73/'"?.($%#8Y&3// +MV35`XOU&)2):`?3!'U0O,M$B`>^Q02!D;F2>P0&(,[I,:XB-4B,Q"1#@=49" +M`J9!R^3&R=Q;J$(0(GD(*A<(='U2"1V&;#_-`R(D;%,\DL$B>Z$;X!55)G48 +M1-'IS&VZ'48&#GE,<C<0@(`:\`QVDI`;[<]TP=B00G:7"<;\A`;M1:2,8R<I +M-&0)@H0XZ(<-(B>4[2[5`(GL902`.:#ID(2,Y$D]D33Z().$(F,'ZH#:"6GU +MB9_*F$SO]$F-)V,%(.Y`D(:'4P0`?LD`[2)D`]]B@#LX:1*<O<&ZH4$C9&YP +M-LIW.#6R'&>Y0D02-.>ZE<\NHM`:)'/*HBDQ.=MYW2C,D[(2XN;))"32XM(! +M@'906+-LEU4DXF(2TD$$.)3AX91:WDY0DM#?RQQNI5A]!+@3/PA<=\@HGNCT +M_?/"AJG$#8^V$"UD8!4E,P9;G':5$"`?5()X4[6.@.VCLJ@2UT`B?E.3#`9$ +M\93:H/I/SA.P-J$``R#NH+%KAVLOD=H4I<9@SW[*(0T>EV/8)5'$N&9![K-Y +M5+J<68(R-TTB=0P4!<-`@(`YVH24$],SF?A"]P#MQG&4&J'">$+W>H09SA%% +MEI(D"=RA+9;D_;!3.+B\DCG9,]X(,.T@;?'R@6Y,]L94L4_X4N=Z7R(`V4.M +MPR2W*=S@:@&P(VE5."?KTR1!)F4#LB"-LRBG<B2[L@>YH`$85"!G@'ZIG0<D +M093'&<#@"4%0AI@D`<`=T#D1F0/N4P!!@#?>>$S8D3*L=2_@!58+)U4MTC6: +MD3JYVX00'T[YS\I4W`\H9#G"8^`D\M#AZ9^40Y<7'@?.4#@)!R24Q,]FI$ZG +M$_W5#XW$S'=,UHF(W0DB#DY3R0V29(W0"78)*KU':R`9]D]=^O\`*%&60\1P +M@,#5`VGNG`:T.,9!30`!#N,I0!N(`5*.B*/G!U5ITC)&<_91N+<D-)[0FJ/D +M:08`[H]#O+%0QI)@9S]MU`#1+A,Q^RE9'FD$'M'91@MU&-^ZD9N-.,;JHNTM +M(I@02B]/8H:=1K6``''LB\T=C]D.%FX.EKA@B,$JE<`AT3SPKEXZ"1&?M]53 +M>!J(G`*54-6=H"B/I?MDA2U3DG&.%&]Y=!+H(/W4`N_/@&$B6R'#=)[AJ(P( +M0-$/@"9303HF0TR/T2:06!I)E*3!$8E-K=K!``;QE`0#9`P8]L(&_(CV*<&0 +M<C.X"9Y]7QB0B">.9.4[OR['L"A<1$`A&_\`(.!.854F-(:?4?A$6AKX[;Y3 +M4G.#,[=RE.!(CNB'=&X.R8F``,)2X',0)"B)).T245(..>=L)B<D`#M`3:M( +M!=,@)Z1!?F<'_50)K1)!Q(V[J0#3B#F,(6.;KV(Y,%)S@0UHQP54['4<?B,H +M69)!/M/=)I(AS@8VRAD:P!CNH)<O/ID$<)F$APR?A-JAL2?NE,9P(RJ&=.H\ +M&=I1-$`N<2)4=1\F03_F4=,X$$E`SP-1$D0A$C&(/.RE(:7.<-_?A1D&8&_O +MRHIWDZ2-1&GE)C@TN!.3^B3=&D-(`_NA,-<X!V"(E$(;S(SV1/:]CAK:1(D> +MXX0'<B$;WZC@N(C??"*6'")R3_LIVN`S(,(&N$#VY2$G(;DX0IR3[2.R<.DF +M!!_=(D!@')R?=-/IDD2>R!SAP),>R1Y(SV]D+V_S2V))YE,T:F@`X"J)&2UI +M'._*1Q$&/<!,(C_JVV28(:<0.5%Z%J)!)W`R5&).!&F4;@9,Y![<*,#!,[[! +M!)IFG)?Z@8CV0Y`T@C2A@3NG;Z3\<DJB2GOO\Y0S(`(!)[*2E2<^D][`3HR2 +MA>7:9,`;0H&WR`(.(1;;D",8*:)Q)D;F4B#.'8!GL@?+7$'OPDV"_.3.Z6"1 +MM]1E.ULSD2,90$TES#\@83M&3&W=,USA$R0=RG`T@.!WVA%Z$UQ+CB!Q[I.( +M.`([^R<.EFW/*`DEYDX(@RB:%.8,&/V1MD8R<*,B'`[QPIV#^43D#]D`Y#H. +MK`Y"*J#$F028DY4?YJN,`=N457!<=I&<H:,]V)X`WA'3#L'^F=OA0@2=AE3! +MIY=$F-DIH3CJ?K(V/")H$>K/919@P1DJ5^EEO`(G(W]EEI%5(+ISE!4=D<$' +MO"6OU1Q*:)=G!Y5036G7)VVE76.<+5S1I')=L2J;"00`.5,S\D@C2$^!JAQL +M('LCH,<&E\@$YCZ(6-UNB7$!2@[-&`-]U*';.2#`CLE3'I$@'O/"8P!G'=%2 +M=J>9^<J*>L#$#$8P@:3DD@J5X<##?Z9)([J(?F_+)B<H'#@203@9P@GU';/9 +M,V&R0,)V9SC&4(=X<'#,R.%&3#0XC$P$3\N(`V^B&GM@XVGO^BL!C!`&GLG) +M:W!@Q&>9E`#IG<`\2FEN=L?JB%4($Y"!Q#8&1[]TG26`Z<`;=DG0&C[HH0,R +M<?)Y35&D.(Q!V]T\C5($@#(*C>('(.\*H?`@'O.-T+RUOIW,Y@J:XHU:5.G4 +MJTRT5&ZV3R-I^X4+H+B<Y']T`O(;Z1N.1A*GIDNP2>XE.3J('`Y"6&M^NZ:- +M!)RYV0"G!``&DB1DH8&7'`,C"8N],ZAP)0._2XB`94%5\NTCGE27)?2+6N8X +M:A(GLJ\`F)S^ZH8P!`R-Y3G\V)D#^Z0(&,F#O[)IY)A$&T@&08QRF=!:,$3& +MR<^H0TD$_JHWY]!W^J!W-);!&#NG.EI!U9!VRD0)`G/"%P;$@2(0&!%0.F09 +MG*.F`#^:3O""FT$`D[X^5)1G4!@GL54K1HU8I-!F81><.Q48H]W9&,92\D?X +MBJ<+=T&!T-,R,:>%1J!DDQ,=RKEW`<?OO$*C5/IDG._RIEVJ-_J&IW(A0OES +MRXF(X4E0Z7&"@<1,[\DJ`'`E_ORFP=C]?9$1#@9R.Z%_]1`D%$,?R:@WV^4P +M'IP<<RG@@>Y,X1/]1)!`&T(H6Q'..QW0^^Q]D1/],`X2!<<GY0(F""9]I*=W +MJW,H`)P08'MF%)HSF2$03&`M(DP,D),+@(!GA"X:6D#/PF&XP280V1:><_\` +M9,2Z?RC!X1%IRX?=,-R<G&44B#JU`$=\[)Z8]1,!/MC2<^^$FGUS*H36#5J@ +M>PF$\>HC()V2<'!V/ZNZ%WYL#G[J=D$W#=S')'9"-B=C*+,3JG[X2:#C(SW5 +M#M(U>TIB8P"!GE-F<F2.0A(P29RH@HVC?=&PZ03`)G[(2&R`"B$!XAV$#G)S +MB.4+XU;P-]^4[MB0[9`XXU`^Q"*%QB"79C&4AAV^1^R0,#3L92<3&=T"!VEQ +MR8PF$AH+73*0D`XG.4M0:X#2-X0/.CC"(.]6/R]X3"`<[I-;#8G<3]43_"=, +MZAE/,M(C?NDQTMT_E'9(X;D@$HH1(/.T;)VQF<&<E-!UY)`^-U(R0)B1R)R4 +M-&:/3O,>V4[`6QB3[!%I=H+P"&SM*6HZ1'',($\PR(.3@A0P9)VCA&XNG)R< +MPF,$02,?HB$&XD_7NF@',9[)2=CE%`'LBI#5J&6ZX`&PV48>-7JV`_5/HV@? +M/ND02Z7?TG[H&@#W._PB@Z@):1'=.)>_)@\^Z%Q/YG8(Y0%@$@N`G$#NDTRU +MT<\IAB3.3G>4[7"#N0).$47]43.)_1$PB(C$J,8#8@2C9,SM_9-(-@]$`X.4 +M),C`(#433@M]S*&)=&<]D!TA)S^O)4U9H;;AHDSV4%(@`;F.%+6=Z`>$#,$. +MW]H*8DDDG;M"'S#C2,#L93R3D"2@(0'#5$B813J<6R/="P'RR22!P$S8:^)U +M3N2I5D2'3,1B=E$^"[_5&YP:6@A19F7#"FE$`8`'.91%IJX$B!RGI_FG)C,! +M$[\LP?8@JHC@M#OS23W1EQ\L#&IW9"XR?=3T*;=1>/\`,(:%0:6CDR)_LG<0 +MVI^:.=U-TW^&_C!_$BJ]G^&F!)/`SQ*&]=JJ%PHLI@>G0&Q`[+*HR<S!Q$1V +M2I8,S!WRD_@^W9,Z)W.!R$$SJKV,<`2)PHF:@09&QY1-_*)SPA(&Y,$X0`:A +MG&V^439))!B-ONF>-1R?9*EI%3+CGA`FEQ<XR/\`[=Y]U')!(P8'U1@M!_*/ +M24-2=0(CZ*APXQ$^\\(9D%HYQ*1<X52[4<\SND\Z3D$1[H'JDBCD`9W49>-, +MF)V^4]1PT;DD\("XZC'`50@_U#A!)@S.=H.R-[A$0@#@3+>4@=[M3`"3+1&3 +MPA.6:8R,9.Z6`-1.)0D#\VK3\;H?].02[!$D)G3IG4)/[IO=HE(@@29^B:*1 +M/H+7`P=@HW5"`/T`3$Z0=1.=@%%4(+OR[[JAWU'P-1,1@=DP=#L"1L"F`]$$ +M92S)'M`0$[!!!(GE".2?L4A&LD"('=,X1!D%"<B#@(XCW2U9.-\D#,I!HX;` +M3%IF,=Q[JAG'U`P=7NBU!QC?]4="B^L:@:6-T@N.IP'[\J(D8;R#DSNHFA`, +MU!L'&P[%2TY:X\Z3E0@@.#IF.94K&D.#OJ51>HU7-I-;)V1^>[N5'2CRQZ47 +MI_PE39__`%9N'D/=#RP$&8Y"H5B7`Q!`5V]U$G<-E4:N9_:%:(B!I^,B5&X0 +M,G<X")Q,EHP.T*-Y.N3E3M">TF`/U2]$&"9A._5IR1"C.-B?A!:Z76MZ-;76 +MIZQ!@1.?<**Y(\USF&&N<2`,0%'J=J&`.9*62XD.^J!9($../E)PSD'YE,XD +MNDRB+6Z))`)X'"!-B(+A(3THR2<`Y4;?2Z"W[HV^Q@^Z"S>OH5?+\FB:4,AV +M9U'NH-!D$`CW]DAD`';W2DB1/"!'TGA(3J&4P^#M".1Y8](!)C5R@'5#N8([ +MRF$1W[A,^-A)A)L[`9/97L'3`,^J/9.08^>4(])@G$Y"<OQ''N5`4PTQ!^4$ +MD&=7"1<`-MT,Y)_=`33(R[9,'D$]BF<1H&($YRA82XG`[!!('29&%/\`RBQI +M8\E_(55N'0I&D<2(SW0'4\P[Q'NA=ZFSR/U3%W])P=\H*C@#(&>Z*<D[G<]^ +M4GET9,'ND'-)!,@'E+C!D($W\@@DGNG]4G'/*;:8&.Y0.X02$EK<<)-/IF<C +M"%I(<8)!/"(9B.40]$_TPG^1,80L=)V2<1,D2500D$@P!RC`_E[0%&'%QDHV +MN.?[*`M;M&((_9`7&#Z?NC:^&;Y/":H6^6#MG:=D`L).YB?>$U,AI)+)]I0N +M($[3[IV#4'`D-Q(0,-\B/E.2-0Y[H6D'?CB40(B/LB[."7&(`^2CDSG([(#& +M'$Q"9CFAWM^J"0D"=+O_`,(2:0?S8'<(7?D(9/=.TC>4!'_F?&P3M<"P`M)^ +M2A:2?Z))^J<'.PCF4!O@Q`C&TIP&C+2<H:L0#&^$I`P=O9`9?-4FJ3`Q(3L( +M$P#"C`WU$8[J0;XS/!0('21&_=&'M,G`GOR@I-+\0,]\(@2<@"0@8M:9,D@" +M)".A3>\:PUQCF$``@G`)[C"ELW%E20.,%`U>)(,[<)FZ/,!R8X2;-2H07'/) +M35&C9@R-R"LJ:Z?JJ2TQ.P*9CR6^H;8A`6%P&H93M(@B2/A4&7NSI,81:VBD +MT'NA$@#!":,:AN@DID/J1$QMA7':12TM>2!C3W5>T8&F7#<9A2G2'&-H6:HF +M$MRTQIY&(2<YIV,F<GNAV;VG8%,[4!L0$"+V!N9DA/3=@'5*9^EK1DAW/`0T +M8TDR@DU`8!W0X)TR@^1CY10!ZCD%`TF"3$;(>Y(&??=/Z=XD#W3TV4RQQ=4T +MXD")DJ@=,TX'U!&4G%L@D$GB.4G$EH``B(D(20!I`@2@<N;!&P[90MR<'ZG= +M"Z)`F)Y2>R'8F3]95#U`"TZG#V0,PZ8$?*0_*3J.-P4.H3D0!PF@3F1(D%!` +M#?2_=$]K1SO[H"V`3[\(AA^724AEN(":)$G.>4Q`!+I]D(9X$21N8^5(:C?X +M=M(TV`M.K5SMRH@T22YQ([RHZKPY\3LJ<&J$D[8&<8E1P-8WA&]L1O\`"$M$ +MG]B@*#I/;N$AW.2D[)B?\DQ:6MW0)L3!E+2`X<#;*;=VK:$>F6R3\(A%I`R) +M!0./H)X]RG.N8:Z!V"`G_J'Q*+L[20"6N`'*4-+IG;@SE+3ODD']$T@;$_=$ +M2!KB3!)PBI2'1D3P2@IO(;@[CDJ1FJ<F>RHO4R`P`MV]T6IO^']4%/5H$3]D +M_J[G[(BS?.:1O/Q_=4*S@XR($*W7+M6,?14;C\Y&DGY5JHWU&%NJ(^%$7-#I +MR0.$YVWPAR';@1P0L[#.=,D$IAM,I.P/5L4FB1#<HA\%LM.1WY2IN#!,"8Y` +M,(>^2(14?+TDO#@Z,0-S[H!D$B3]CA/Z9P))XF4C`.WW3"`[9OP-T!:O5I.( +M1#,9Y0-`@D'(XSE/I$3O&=T$D9R0,IJD<#`[(7B!`B$=(,\LN/YAPJ'I!U2J +M*8DEV!">H?5$Q'=0OB=C\IV%LB0?NH%J&`,?7=$7-F2?JHW?G@`QS"?1J@9D +MJB1L.$EWZ%,XAQP)]X0-P-!DQ[E(@3B4#D09!QP$[BTO_P`/LHR`'01(&Y[) +M`-@@GX"@,1M(*>`!.('9,`(B-]H2>WU3)^$BCH,+@ZH&ZVL$NCA"!)W.=D`D +M'00[W1:?^HY]T00T[$B?E+3F9$]D).ET.]/8IC_TNYG=4.W!CT@C8%%B9G[% +M`X-<R6N.N=CLG#@&`-&D]QRBE`'YMW?JFP!,@_5`!)U%TPGR9$X*"0P693-@ +M?Z(!,Q)E2TFZJ;R)EL&%-!\0`#)[)$2-OL@,GU`PDP&),`GGLB)6Z2,3CE)C +M!/K='O,J*($@X.^5(&.))B1SA-B2"!I$Y358!R(E#)`.R%[P70W[=D4G#TG& +M90AI&3F4G>QWW":(W(*(-K!L-SW3U"<9P$-*9!=.$_J@DG!]]U03HTB1*%D! +MV3RA;/<3\IVM($3R@-S9'SC*?3&<GV0L<>2=T\S)XVC=`8$"03GW1`B3.2>5 +M%Z_\7UE$9`$\\I%&X"(+H[(@V!+=O9!,M&04B3.`?NH#C5MN4[!G<?"$O=$# +MA%ZM.K<JFSN:22<>T)P(;.#W]D`+RB),[[Y]BH!((!$<;RI`T#^J9$!"T@5` +M73],HF/=,Y!^5*IV"&^P1.!@:1]45NUU6HUC07%YC9=5XA\,ML^@T+NWU.>/ +M^;G8(;<B\1ZHDE`]K33&"'3O*L7`)I``[;X4,$#)P@4X$[IZ;7.J0`F)$!HG +MZE3V[?+]7/)02.>Z-,$1V3=I](*4R[5!^H2?CC(S,+*E$.YQ\IW`SB<[RDZH +M=S$H"\:).)[H",QB8W[)I+1)Y[(=8/!TQPG-22TP2`J%F#W"9@)9'*1=+>\E +M(&,\(!J-,B3CVV3M\H:PXEQ/Y<PA+FEI$X/ZI:R0#P/9`M3G-+-6V8"$F3I) +M4C'28'I("C>\-&"#)[H`@DZM0'UW4FC^5)W/*`N;,@Q.QA25GD4FM+@0-E1$ +MT%(^HYA,YS8$G[%#4>W5G,>Z`J0+WP2T?*!P+1GGA"7Q//NB#M3I=$#N@%TD +MQ!RG:USCI`)+N(3!X)[?&ZBJU@`()U`]U4*N2'Z9/V43_>$^H&3,<E,2#@DY +M0.<F(,IR0&['*#4.47HF0!'MRJA-<2(X*1,E,7``CGLEJR<J!Y($<(0X@1Q\ +MI@8D).TM;W11&/H4],M#7`_1`(<S!QV2;!WF408(SC(0F1.(GE*!,ZM^$8-$ +M4OR&9R^2@%D-/$?*FI.=,PK_`(;O^GV;WU+KIS;MQ$4W>8YI8?V(^BJA[#<$ +MAH:'&8_LJBRS\H_-]"G^CONI*8.@:=D\.[JFCU]&K))!6=<?.RM7;W!SLM!& +M?=4:CP!)Q&.RS5#4<<`"04#G21.4GO<&P,#V35`=>D#;.>R(:);)D92TYAQ( +MA!K]&DXGE.^IZ]1.Z!R!,.)PF<9,R!\IB[N)^J)Q@B0W81D($^7.DD.)RF;^ +M:)@\04P))P!/$[):])!/?=%'`WGZ)W21Q'8*.G.HD;GNB:^#B`"$B#@D`0/E +M)K3&'8Y0M?`!$%*F_.VRI$KJ!IL8\/:0\2,R1F,J-ATY_1,2.3GY3/?+G.TY +M*BG`DSRI,MIZ=,3O[J/5&",I.>"W5'^B"322V<9V*'!=`R1^J8&<B&CV2+A# +M06&>2/ZD0[V@B&#$X3P]N)`U;CN@U0Z2)^4J;QKEPD?/*`J8$8W3G.^.R&1C +MD#9,7("JZ]6EV^V4I]6T=RH^W/'RGUB`!L<(I'7$YSRG+8XF,X*'40P`#,I% +M[33].LCW0&?R`G?A.]L8&2>R%C@21$QV3Z@1IB/D[(&>),SO[I>Q$)B]H<1. +MV)28<@%THAR8;M\J2A7?1:XL.7")W46IH:0D'M+<`Y0&T^H%PD'=&`"2"0!\ +MH&N!SF2,E.UX+>/HBBI-8714<=/[(]88``R)P8*B#MO5]$S'%I)X/?\`[()G +M?E(#<_J@(AA`S[HO^:XBF)`R8[=U%JP1(A`[LNVVX3M:#]DVK.0!'*9KP'D[ +M(@H),`E.[:(@[Y3,(U9C&-)2>9S@1P4X4F3I):)C,)__`,4)VN&DQPA9$:9B +M>$T'`R2'8^$;&SZ9^J'>=(,?ME%OVA`U,,#<C?ORIJ=,:H,=P%'`<^1D=R-E +M*P@-G>4`09+@G>`3@$?(1@M!)(WY"3CB8&>4#.C@P1S")T!QS(3-=C2(SR43 +ML.:"=N`B!,1)<E'JD1!/T1:(/K*38+ITF!^B+#,<!M@\93LT3)=D;!$Q[F&6 +MN(/',J%I&F!N?E1IJ^%7@=<H/<]K!3>"3JA>B^);RA4Z'>U650PD"F`>?9>5 +M,:0[21!"MUJ]>G;MM:CB&-,D'!!*K-G*.H3Y<;25!+M1G96*CFU&@-!$;F=U +M#6;MF)X470:;"]_J,0%9!ETM<"/=10&MW$A'Z2"1.,PI06HM9I+M^YPDYVI" +M-(W)SPDV)&!CLHIH<3))_P`DY=-/1((!^J9^_?5]4[9PUS`9._=4`TP,#;A* +M1K./H$;O*#X9VV/=`"0\@8/)0(@@0D-3H!2V.9(F/A!L3R$!8+#$2-\;H9(S +MJR.`E`TQ.^Z1;Z?3QA`F/(!=@\Y3$MEWOLF([;'VW0D@$1N3,!4.T@C+FYX3 +MO=#(:Z1V0.$,G8<ISH`B2>WL$`Y_J'RFT@&>QV2<3'YLM3``3!^$#$$3$)BZ +M&C$\)-$P(.4#W9[J@G8+CO/"K/WVS\J0R#O]"FT:<@C=$!$X''*3\")`"+2! +M_6)[[(3ZAGO"!,@1Z8)[A(F#@#(^4Y$@-#I*1WF2/:4`N/K&!/LGT`.$I.)! +M`;N>$Y;@B9A$[",_W3$>D0#*(&"?9,V3OP@30`22#^R0`DX3_P!)DI$``.(P +M?;*='!I^<^Z><P)^I3:8.K_91.R9:,#.4":[,B1*GHRYP)!+?E!1`#@:@+AR +M&F%+0:21)`GVV6D:%N'&BTAKG#O*/2__`.-WW3TZ)+!,CZE/Y/N?N5>$X5+W +M0U\`&!LXB/T5&IVP<[;<J]>Z=;HG'?95:H<6N&J`-_4LU5>J6R=(`GA`UPU3 +MSP0C<,'T_=!I,@$1/"BE-,$:@2W8B>4PC2X:=_N$B#IEP&<PA:(+N/E1"=#6 +M:MP.$HT[Y]T0$&/NF;)!Q'P531_26@#9-@.U$Q[!.#)R,PA=F,GMOM_N4#\2 +M`#/"+2W48!RDUN9F<[SNG9^4`JA3+6M+6C3LAV<=Q"*H/5#<M]\(1EIWX4!& +M)&F(.Z9X:#N93@&,!$]K#&)PAI&V0?T@IS);,SRAW=O`E'`#9VT^Z!-C2,`# +MW0L#6N]61S[HR"!O^9"\^H``GZH$/RR8R=DOVC"+27F2Z"A].@"<E`XV`!W[ +MI$RR!$!/R0"/=,\0W,YY0`"9'Z)G#(AWM\IR,.)&>/9(#U`SL4#M#G"29(]\ +MH2(@29/8HV-BGJ.T[IG#TG=`-,0W&?:43@Z"1J,')3.PPC^KY39`]C@^Z!P6 +MP<;^Z;MW[IS,$AWU*0F2<!`V8G`A.W$D1E.(CY2<X&)@1&R!Q,D.(QE.);C8 +M'!GA"UL9))X1G\QB806ZUG:MZ+1O67M-]>H]S7VP:060<'WD*FP;C'R1*.LW +M)#H!&9!0-(:XG3(CN@)T$0/J!RBTC6886G_#RHX!_-()W2)@S/\`F$!O>3F( +MDS`$(#(/'RG:9.2?<)%OTR@1W_1$"X/:0)0T\D_NB+=9@`YW0+(G5B0G/YRX +M#XW3D:J<$NG?;W2U$$S!&^/KR@31+-0(D',82$:-I,X"=K26''Z[IP(=!=C; +M<_Y(IQ/L9C*)C9:!)SPE,8TS/;9/2:X#L9F5=(3@X``#'"-S8.7<*32UU*)R +M.R'2';MF,RHIG4R,$1&^=D_I`&,C*DIM+O:<H7:FNVV32&(UAA=.>4=4`50U +MC1,3/=,,.D`X_J*>OZB=R1G]%+RL5WODZ1W&,B$X<YIP9U[R=T[V&#J.V,(' +M-B()E72K%!Q:&N:8TYG?,JQ>75>_O*EY=NFI4RX@``G[*I1IR#@>YE2-_P"8 +M""94T@2[33,`R!A/2.LZS@#9-<5'U:HF2[8'X&%)IAH:(]/92KHC(VF>X128 +M$GU>R%V'`<\II.DP"H$]TOB?5VV2<9&J3'9-!+06DB3ND`YQ)B>#[JD+5!!^ +MIRDQVH@.<1[IIC9N/W2&",`DG<H:,20^03.^2F<0(#9^4X):`Z9B2<80@<DH +ML$YP#1!SRD#G\T=D,``:O@2?9)\AL''8]T$]Y1J4"P/+':FBH-+@<'/W4#7D +MR282<6C`,%VW*$D-)@&)'/*0.7G43P/9#K<1,C)W'*;?;[IL#DXW5#Z\:=1S +MW3/)U#)CL=@F$N<&EPR([H2TAK@?J@(%T8=]$Y>7-`D1*3,?F<)2):_!@>RJ +M&&,Q@;J#S`YP=ICV5BXKTS0TM;\E5?1,D'XE('J/$Q(@\!(U(;&H#ZIBT`3] +M0AC$%TJAR\E\"#'.R;4!L,X3`AN(XR4(C)@[J`G.)&`W`^$['2\@C(&(*9I@ +M3!/RG8UFF9W1`EV9D8V2EQ=O*8MGMVW3Y$@1/[H'UX2:X3!/W3``M^".4VSA +MJ]0]T!DB3`QLF>['.>R;TZ@0=^4PWD@F>`$#ZL1!@29[(FN(//P90$3,@0>" +MEIT^D9CD(B4.'((CWW4U")]IG95F07'2I:8`]6Y$*PZ:U!X=1:9C'9'J_P"K +M]%%;,8Z@USG"2I/*I_X@IJ)Q^%>\@2,3[\*G4]3L`EQ=/*O7H&HXP.9W56A7 +M=;W(JM]4;`JU52K&DP#W`0/!TC(R.>%(]TDDF0=T!,NF?NH!.^<@X3<;P2G& +M"#''=.W2=6K&,(&+L'`SW2&_"8DN@0!'LF9@QD#V0.#B0V9X1,:PYTY/NASP +M"DW?)_1!)`!@SGF4U,G3S]$1!+@&`D^PDH6983G"J'):3GYW2DS\(=68(&$1 +MP=OD%%*8$9R>$[SO'9,TPX#VYY0D^QR@7],$E/3U!TZL^R8`:9RDT9,X]T03 +MR"P;GC!W34R2X8^Z1([8Y2#@#Z.$4XG9VR0<3)SE)I!$$`9V'"0/](D"5`[7 +M'5+C!'LE4<7.`WX3&/,+@/NE4@B6SODH!;D3J/.R<[S((Y2&G5D3/,I#\\EV +M_(0+48F1":>=H/=.XX/J@H9&0#]^4#D[N)D\IB&D>K<G$#"$D$#;4B=+J(]4 +M0<!`G:M\"4AG,#'*%ID;\(VCTZ?N@8.`YCLBWR(E,6@-[IP"0`#GL$#M/JP? +MHB+BT1CV0AL-D;!*,R1\90'J!!).3ND"-67`-Y($H<EI$0$JA:'P!I;VF4$E +M4L:]S*1+F#\KR()'PHBWTZL0A!!/:/='Z=1AVH#F$#`RZ=S[(R=3VAVW.5&" +M-1D[(J=33K#`(<()*`B8,`9"=A@!TG[H9D?YI.R`$$K"?*F/9)KA,P$$^C3/ +M.#E$T`28P,*@Z>\3G[),P@88$"3^RD8)]_K*`F#\HTJ:@TEQ#),]LRE86]6X +MJM928YQ=@87J/X=?A_YY9<WU.2=@6[)&<L_5Q'2^@WE[6:VC;N(//^PNQM/P +MZJFS];8=`V^5[!TGPO:VM`-IT6-@1`"U&=+9Y<:`I<Y.G*Y99/G6^\$7MLXM +M;2+O=/TWP'U"X`=4HD-'?NOHJGT&E4?E@*OV_0*#:8_EA3WA[9/F^\\`WS&% +MS:9)VC"I6O@7JE1QFB6^T[+ZAJ]!HZ/RC[*J[H%!ACRQE/>+,LX\!L_PUJNM +M]=1V>T#O\KG_`!7X3J=(`<8=G;_9]U]']4MF6EN8;@+R[Q9:U>J=<IT?+AC3 +M^8+/OOX8V[WMYYT_PU=W-EY[:1B"29QNL.^M7VU=S'M(B0%](])Z)2I=)#?+ +M`;IB(7E/XM]&IVMX:E,0#N`KN6-8YW;SZBW2#G!*,["#./LB<'#,R)Y4=36U +MP(R#NHZA<[47$G/)2],2TP(2(Q+DS<M```[*J>F?3VSLDT""9,'L4+02=.T= +MDX.D$9^2@:63J:=^)3/#0,'/'LF<(,`3'ND[6!$R`-]T41_(,XA,T`MXP/[I +M9&(!`0C83"H<Y$'Z)$-!`)GVG`2>.8$'CLE3INU"/LIMJ8VG:W40,8F$VG!/ +M;:5.VVJ.W!RI_P"#TB"TR>W98]X[3]/E5(0[D8'"8@-(@Y[=LJ^RS#:8)$_! +MW41LW;_V5F<,OTV<4F:-0SGLB>9=C,;'LC?;5&Y:PP.0=_=1.I.#B-,'?LM2 +MQQN&4-Z`T$S*CJN)>&C')37))=C!43B0)'9:C(G!L8@@\(2T`P<1W3-,Y<,> +MQ2<Z`<[X506ML@$`0/H@IMR`?V0@SG(5[I5A5O*=>HQ]%C:-,N.MX;/L)W*@ +MID8D&$.!DC/:4[QB3./="#O\;2G_`%#@1.?OA$T&1/*!L3`XW`*<R&X_?"*$ +M@D$=_NEB),]DP(U29B$1AS2[,#W1#``D@\)VC&`@!DD*6UKU+>X;6HF',Y(! +M_0_*&T9;G&VR+&C^W=-J).)!/9-+L$\]R@?2"Z&R`=I2:1ORFGN8^B=OYL"> +M4!TIF6_*E:,R-Q^JB;F"7P/A2TR"<2"#NK$:-!O\H8_4H]/M^I0VU1IH-+G- +MGW"/73_QM^RNS:.[@SJ='SF51KENJ`(^JNWS0'&#,?,*C5B-QV2FD3C!P/NA +M<0'3(1.D849(Y60GYV"8@!^EY"9GYM]CLGJM(JG5AWNBFEL@!(#.0CI/%-^L +ML94'(=,(6[B-CW0(!NN#D)-_-$8&Z;!?,?5$&P/<Y]D06H:P`Z('=(G,QN4) +M(VREZ1!C94.8)G^Z(!I.K5D[H6@'(3@%K0`,*!G$:O44B!$SONAJ`.=$A(M: +MP01QO*H(8R4XTQM/U3AK74\[\A,2'1/`C`"&BALR"1&92(R8_P`D/]$$_HG: +MV&]OE`4;;DI1G?U!"<`:1$_5,P'.,J`OZN\)$M+3DI?TEO$]T,$[H";$3.X3 +MC?<;(3\Q]4MAC[RJ=$6P2-0W^Z%PTG<=MD3@0W>.Y*`[$%Q^94-$./5&=TY] +M6"YL<Y2HZ)/F:M,?TF,IFQH@$S\IM3@\DY'9&V`)P2HHQV/>43>8)RB#P7', +M`I,,`_XB<&4V(@G*$>DY!)E!(UKC@[)SO&J!W(0M#B<S":3QF-I5!-:[:-^R +M8[P8,)A)G@A(MG<[<HIW-!P0DT;`1!00`1,_*-HDF'8^Z&R>,Z0-]Y3M;!$P +MA=(P(3`N/930D='E9F>R3!Z)(0N)P)Y1ET",#LJ'@AHC:.Z=F6\Y]T+"Z2.% +M)1:[2=O9$%3:&-V$GDJS86M6XJMI,:7.<<>ZKTR3Z0)/<3*]#_"+PU4O+IMW +M49+`9`(/^2,Y9>LVZW\*_`=O2M67-S3#ZKA.1M^B]8Z+T^G;L#13`^%'X7Z> +M*5!C-,8[+H*-L)S"YY9;<9-\T-.V!`X/LK5&U!&6Y5FVH>D<*W3I:6KFZ2*= +MO;!KXC!5YM$%L$;?JCI4I,E6649XW4VU%-U$`:0%!7MQH/9:IH]A@J&K2$?L +MAIS'4^GBLTC.?9<S6\/4OXT/#?RG>%Z+7MY9$96==V6=7]E=LW%S->W%.V@! +M>7?BY8&O:/>`1&Y"]BZE2#&$'8;KS+\5*M)G3*PQLI+I9'@]]3%.I$[<*M5' +MI;DS[*U?5'/JN(V!RJ[W=FQV7:.J"7:I:<)Y,1@E.3#C.4.2=DL4XYVQ@X2@ +M%N9)]MD+I`PW"0=N8,G$(0Q(B-N\),#G#TR>PWE6;*SK5ZA#*9"V^G]'#-+J +MH),<KGY/+CCV]?Z?]'GYNIPPJ5K7J$0U7*72:P$.<#';9=*VVI,8T-:,(W4Q +M@:<+RY?J;>GU?'_Y>,_LYUG23H]69[*Q;],8QTP#P!V6Z*=,4<MR=D+:3=1V +M6+YK7JP_1>/'J,T68G#1",6L[;K1TM),`)F@%T8F%CWKK^S)\9K[2&@C'OW0 +M&W@"1[[\K7T^DN@`*&JP.PWZ*S-,O#BQZMOI=(;+NTJI6L0]Q!$'B.5N5:32 +M)(4%1AG`77'-YL_T\O<<]<=-)+LQ*IUNG5&9'J75>6W4<`_51.H!P@M77'S7 +MZ\?D_0XWF.3-N]H/I46DY#1F5TEQ;,)TN;LJ-S:-)D-A=\?)*\/D_27'IC:3 +MO@\PCHO<UXTG'(G=6*UK!D;J$L+"?3D"#(727;RW"X]ANG:ZAT1!V#0H7ZHS +MN.4;AR1`[$IMR&@B!WX59,R0(B$JC3H!@P=O=$X`#?[%,2"`V8[1B%$T$;&. +M$+7$"!,?LI'L+'%A,GF#,H'QJW0#,&`#)1&"/3(^J=I9`,F1M"$`$R#E`[3! +M@B2@$]RI-W9D]T,2^3A`GOU&3GW2:[)U`3\)/P9DIZ?Y/S$]C.R`Z8!$@?JI +MZ,3"B:`UL$`2I*>F>%J(T:+Z8I`&E/\`^)%YE/\`^'_^=/1T>4/[(_0FDVCN +MR&/CN=E1KM+GX`#5=O<./:?LJ=?6`!CVREYK2*N"&Y:-/=0`1O!5BYK&I3:S +M2T>6W3@;_*KDSF!\+*[`\`"<]R$CDYA.7&#(&4P=),`(A^Y`S^Z1)!G0/A,= +M]Q\)&#C5&$#P#Z2V9X",`!LM)'_2=T#7%K@1N<R41<XF9SQ*!C.F(,_"<-); +M._(2#AF1/]DYB=A@<(A-(TSRD6D`&##ML)R=+<_IRDYSB!,EHVRJI.#1I.#( +MRFCU2X9X[)$MU#&W"=S@1!;[94`2)&"B;@Q&1R2CMWTZ-4.=3#\'!)$'O]$S +MSK)+B23V0`1F0#_FB:8;MGB4TM!RG=AOI=/?*H9QSDDG=,/4,XSR44EID8CN +MFJ/UO+XR=XP$#[@;F#'T0N#0?E.UPTG)U<)G$8).5.@PTQ/Z)R<8.#PFF`3* +M?TDS.$#/`QZOHA/IR8^Z,D1!^Z9SN`90"T[%I_5(CG4$<&20<\?"0'HG(A`! +MCO/N$1+9].P3-!,G)^B,$;($((D&<;%-I([81,AN9G.R<]S'N@'$#)2:6@GD +M)CM&"=Y2TS)]\%-!.:0V3LG;H(]3@R!RF</1._&H)R&@X/V"`>#,2B;!SB`/ +MNF8-6J8QF$YTGL([H%)B"A8)SC")FF9)D>R1&"-`^Z!FZ2!,(V#!C9`S)C`" +MDIZ<D!`J>)&H`*:@T:B'\;94+&@G8#WV5FDT;8'U07NAV#[J]ITJ8U&HZ.Z^ +MCOPPZ"VRZ328&@$-$XB5X_\`@OTT777FU32U"G]>R^DO#EJVG;LP-EC*Z<L^ +M;IJ]+M]+0TC;W6@RB0Z>!W0VS0T",?"O-8'-Y*YTD*W;F8PK0IEPD;)K.E(R +MKK&%K0UH$$9*EK2O18(^%<HTY;@<(64X*LTQZ/[J-:1!DQ*"I1!G4"K(:/CV +M3$`^R&F?5I`">W"IW5'!ANZUJS`!)^%4N*;2,$PAIRW7*.JF6E>7?B%TFI>T +MJE!LC5B3E>S=3H-=3,#9>=>-*#VO<ZEN#V4MXX23EXZS\-R\ZGW&_LJ/B?P$ +M+&U%6WJ%[HV*ZSJMWX@H5GOI4'>4WDA8W4^MUG4]%VUTQDA<9Y?)/CO/'OJO +M-Z]I5H.TU&$9@X4-1@$P?ONO0NFVEEUB62))W6%XR\+W/3'&K2!=3<=X.%Z, +M?-+=4]+O3EG#T0>5+:TV.J,&K$[!.:,#U'`XA36+:0K!I#H_ISRM7+AZ,/!K +M*>SI^AVM(46QDE:1MVZ9)CYV6?TJHUE-K08+1F<J[4NB[!(@]E\O/?L_5^&8 +MS"2#K:1#0&^G$@;J!\<?HC+@3OOOVA`/S;Q!VE9D=./@BW@N.`AT@$S@QW1S +MQJ0%PU$`[JZ2TU(@$SOV3XU;X0GY3L.8_NK(S:(&<$X0`28F?JCI^IV`#/NI +M&-(,N(CNAPKU&X.3*K/;+B3^ZON#"X2H:M,3(:?E:QK&6*F*8`)X*%U,:O2, +MJT6:A(S\H"P`8'ZK<KE<5%],&2,=U!6IM..ZT7MEN,2H33!DQ,+4R<<O&RJE +ML)G2/B54NK=K3J#9G"V31+@8;!*KU:+7&'M,QN,+MCF\OD_3RL"M99]+8',E +M4Z]%S,1)/*Z!U,ZC#3'=05*#"TRPN]EVGD>#R?I9\8);D$@X]DM.T_<K3JVK +M"8S`56M;N!,"0NLRV\>?BN*N0-+3DGDJ,@&9;]U+5:1(((`P(49;%,N&\K3E +MK0?2#@(FMD3^Q0AI,G^Z)K=HDQP@0`/TX2.()!`*6-X..4BW$Y^VR`<$R?OE +M'#=>MS0?;9#_`%R<_=2>HC),#[HA4V@O`!&3B2K+:3J54L?^8;P0?V5=@+7X +M&_<*9H((F9[+2::-$GRADHI/<IJ!FBTYVY1K)PCO8#03$DP1'"IO+7#3'&X* +MNWAET-[[]U1J@9S@SRK5TKN.#&^V2FU`,DP>$50P)(!08#)].4`!P:9W2,'( +M@?"1;!QM"1T[&8X4"F>('LD73N,\)$2<"$@UL[[=@AH[@W!&4\@`2AF,-$=D +M0((/I*!1/'PGJ`#&_NAV`(^ONB@3,Q`RD*6F(S[%,(C!V1`@$1&4(.8T[?J@ +M)P&#P3PDZ"9@;X3$0X#(3;-R1WA`W<GOM"DHEC6G6P/!$"9$'OA"=)<3D3RD +M6>G5M&=^(0(PYT`&#R2F`@S(ENV)3P,D&1$(20?2#))A`[LM.WP.4M+3`#ON +MD/T30"[&)0.1,YVRFB&#YP$@")$_F2C3SE#1')P/CLG@;;)FQL#LG9&,F!C? +M=`G&#!,@]D,>WU3Z(&K,3"8?E(!S*!P($Y^=TX`(._<Y0G4($F-\I`$`'7(] +MR@=N''2GTX!.24IWGD=TL:B03E`^F'1V]TSH+AJP.4A&K#DQ'O\`=`FALP#C +MNC/I(@QVA1P6SZH@I\H$<@R[Z]TV8+2222D8!B/HGAH;((<>QP`@;40R`1ZA +MF0EN`(R,$IB"!NB:-I/&R!B"#N91-P"<D)1!C]$,'43DRBK8J6W_``KRO()N +M/,U"J''#8_+'SR@#G"WT;MU3]5".YW^45/5,;3V4T@F`AVX([>ZN6=)U6JUC +M#)<8WA4QJ#N-UT/@6W;5Z_:-J:2USQ/;<*I7M7X)>'/X/IE*K48-=4!Q/.P] +ME[#TVDQE!HD;!<GX2ITZ-A2T@"`/V746=4'2&F5QSRY<<8U:#"2(,1NM&UI2 +M`9,'=4^FM$!QC9:UM3!`@X66XEITVC:?96*;?Y1)Q'"&G3B".%(3(`;V4K41 +M'+H!*FHR8$Y]T-.D7.!*LTF!A$Y[*:5&01$HFM&G5^ZE=3!$\)FLTM@YE%0U +M&F/95+FG(GLK]8D^F8,*A>`G`A+1F7X]!,?*Y?JMHRO6ES!`.5T=_4+9!&.Z +MQ+BH"7$B(&%C*FF/UNWL'VIM/+:`X:0LNX_":UZ_T\MH-(J.&".Z@N:[W^)M +M)R`?[KU#\,NMT+6[;0K.&1&5Y/W+A=UWF&^GR?XZ\+=;_#SQ2VUNVO;2<[4U +MYF")_P!%V'2[=OB7PT0&@E[(F)S"]6_\9%C8=:\(_P`51:UU>V]0<W>,KRK\ +M`:NJU?:U#,9S\#"WGG,M91N;T\?\7]/N.D=<K65=NG0XP8W$[K*MWO-0.&8] +MEZG_`.(GI/\`_<MO6IL'\RG!('N5QW3^FM92:"UTC,%>F>7&8O3X?#Y//=AZ +M?4K-HB08]E>I:B(_0\*:VM<#TG'SA7&VA#@"V9]EY,LY:^]XO'<<=519J),Y +M/"-I>(EH6M;]-J.:!Y#B79P%J],\*W-=H?HT@;:@LSGIK/R8X3=KE1KW@Y2# +M:T?E.>5WMGX.`<#6=,C(6W;>%K%E-H-%KL025VQ\-KP^7_T?'A>.7DP9<$C0 +MUP=QA6Z'2NHU73Y+W%QDSRO6*/ARRI^H4F`]EHVUE;,9'EB`NV/@GVO'Y/\` +MUO\`\QY78^&.IU&RYND^ZL,\)W^DAQ:#[+U&E;T22)$=D?ETI(C$+7[.#SW_ +M`-3S?'EG_E&]TE^H'YW0N\)7;F>H@$GY7J-S3IM&1QM*K-8P.VW[J_LX)_\` +M4\SR^X\*7S#Z:<@'$*"MX:OV"32,[X7K;J3'#\L(76M$M]7V3]C%9_ZOE^O% +M;SI=W1,&@<;JE5H.#9#")]E[7==.MZH(-,&5F77A^S>Z/*;]E+X)\KMC_P"K +M_P#J/'GM(RX`905*(\LN*]4OO"-BYN*<$]ES_6/"#X)MS_\`A_V%F^'+X[X_ +M^CXLN+PX"XI2,CN5"6MC@SA=+?\`AJ^I./\`+)![2L6YLJM#4'TG-+=Y!_R4 +M]<HZ_N89\RLNK3(<9`(G_94+V!S",Z09VW5RX$R"9C8*.F*8.W$K<KAGC*SZ +MELTTY,0J-Q0(>0&Q[+<K,!(`[[J"K2!F``%N9O-Y/!*PJP+7Z2/RA,UWM"TK +MRW#Q``(6?6IAE2(D3DE=L<MO%Y/'<48@R<X1;'<QR$5%K#J#Z@;,^J#PA<`# +M`(`[K3D$:2,?:=U+2<`-R%&#)U'/8(@&F26^V#$%5!-<"[.2IJ)$ZR"H`&AH +M..>5-0@$`^^%4:M`32:0#$(M+O\`"5!39%,>MR+1_P!;E4W"OC,@M.,22>ZI +M5#L#B`K=WO+HDA4J@:79'ZK-:1O.O/W05`2X"!`'Z(X(,EP@CNHWY(P84.@N +M@'WW3$F(Y3R0[OREJ:701`Y0"Z0\&1!Q*41#6QMV3.,NW.F4@<9&=D#_`&WV +M*D86B3@8Q$84<B(V")D;DY0*,X_[(GY=)B-I":9($#Z;)YR1O"(?!`@2`>Z9 +MPTD2/LG!YS&WPD\!I<6@ELX*<*%HB=]L>Z?!+2XR(C'"9I/`YW31!COE`1]+ +M8WD\X2<"1Q`.R$EI@`00<E.8\LG(C]4"<T2#L/;)A*)',SOLF)$B),^R0(B? +MR^R!#`F3V3.$.)S]4Y,C`3#!GWD\H''S@#;NG<6N@1D[(2<D@C=,XP`<24"A +MP+FDG)@A24VDX+@,')/*!I!;O]43G"=P@3XDC;/="V2T2!ON4Y$@"4P,-Y^? +M9`S7=C(2+A,$[8,I2#J[>Q31Z<9`]T$A@S,8&$FY(`CZ(0!(V$[3.?A%)C?< +M(O18#?HG`+6->Z(?C[(=Q^B=Q,MD^PE$,[5P<#DIH)GDI5!#N/@)-'K$.F$! +M.;I9)<"(G!W48G5O$G8HW3DNV03!11D-T$`@&$@[88QN"DT"&S@E*/1C;LH@ +M00\P=OA&=R!C.)0T\&(W3D'5![JZ#N/!,(V.B21GN$+0TF"G.V=D!TX)U#E; +MGA&N+?KEO6C#'`[_``L*1O'T6ATUPIUF/R8,YPI87E]0>$KYMQTN@YCL.`./ +M@+LNBTW5-,X$;]UY5^"5S5O^ET0=F`"3/LO9NA6^BFS&?V7&SEQC7Z;2AH)X +M[+5M@T&.V%2M6P,<]U;HNR,@?59;BU(:TYGZI4A+O;E1.S$!3VPVD*?6DU)H +M&)4C&2-X2:T1Q,(I:,3[?"H<#2()F#RF(G`10#A,[TLD&(10.I>C5&V.RH7[ +MM+'9V"MW-7T?FC'?=9=\^=0=RH.8\27[:3RW(E4NG!UY(:/S<!0>--+:X(=R +MH/#W6:-D]_G/`U#!*\N>?K77'';,\16'\'U`U?ZAF"LZTZQ4M[X.UF0Y:/C? +MK-"ZJZZ+A`9!(^JXVR+ZO4`."9)7&WV:ZKM?%]Z>I^&JE*H20ZGM]%P'X:6M +M>RZQ7-++02/U"[#J=84^D>6#)+8CZ*CX2LC1I5*C6$N>2DWKUC4NG+?B]KO. +MJT61EC>WN5@]'Z#>73FZ:+HVD@KTR[\/_P`=U'^)JTR2T1!"UK#HK+>D`RFU +MH[0N^'AM_L]W_P!#'PX3'"<N(Z1X.>S2^X/P`%NT/"UK3A[J;3\C_1=2VTEA +M):.P3_P[BT""N^/AQG.GES_]'RY_63;]%MFL;I8W`X"M4[)M.G#6A:5M;9DE +M7&6[-.6B1NNDQD>7+S99=USKK5_F3"E%M4$`K;-!NK`1?PX+IC;E:<K:QJMF +MXM,#)0T;,S&X*WFVX.XD0B-NP"=@K-);6&VR.HN.(X4IL<2(@96KY0+@0%.R +MCZ0`?HJCGZE@7G/"B_@M.=,KI'T1,!JBJV[7$@A6)ISC[>IO^BCKVSRXZ9A= +M#Y!DB(X0U;<#>)A!S+J%5C9(.$#608(GE=)5M6.'TX5.I9-+L(;8E1C2[2!^ +MJ@N*`$P-^ZV+NPT9`YW55]!PG5'=-&V'5M&5'9:(&=EG]5Z!95J;@ZBP`C<! +M=$ZFW7@?9!68'$`-SLKTLRL>:]:\#TGS4HXGC_87-=7\)7EHPN:USF^R]IK6 +M\#+1GV52O9TJK-+F@@[@J62]QVQ_4YX_7@%6UK47N\QKA'>5#7T"F8&W=>S= +M?\,6EXPN--K701@;K@O$_@^ZM];Z#"?CLLWP[YCUX?K9>*XQ_J!=V&RJWE(. +M;@;[+4NK*O;^FJTMA4[ILM(VCCNL3<KKGK/';*:`TGV3M<`UX+=3G#!).$SP +M?,Q(0O'^&?HN[YUT%FG6(!$^Z,;0),(8(@9]RB.#Z096D$&QG)_93T`T/@F( +M&R@!AAD%2TCD>J!SA-)PTZ6D4P"[CN44L_Q_J5%3<\,`#G$!/KJ=W*^IN%>; +M%Q<X=O=4ZK6A\R3"MWC1F-]U2JD`ZHRLUI&2!(^JC<8GG^Z-T:/?Y0R`T&#* +M:0!+3S!CE,=,X,]T[FGG,IN<?6"H!D0`""C$%\'O!S"9TMXWY*$9@@GZ(IW` +M`;_*3?I'$)-TDAIU1.1,)V!@>700R<2J@VMP,GV[)%PWD`QLE(V$I.;F"443 +M1C3(E#4WXSPGW.!OB4+L`0B':1_LIY'F#\H'OE`W?:/=.!P<\H=E4TS$G*6) +MTB3!W)0U`9B1&Z<!LS(C_"$#X$C8>R3_`$`01!V*$@%WM[2A($3!440+8DP> +M_P`)-]7'LFW$@@$<).:X$#(,3"(<Q`,)C@R3LD1D28"9X$;P@0@-C5&K>$3= +MI]D.B&CU;IZ8/.?9%.W2'1$F.4Y'9XD\H""`08![RA`)[0/=!+H`HZB]KB3^ +M4`RFQIB!\%#"<?E`+C/(*0.T3`+H]Y1:<8.$&[MOH"GWF(&(D(:2`L-/3$.D +MYE"3G(VRG8)S*C=^;V"`B`8[\IM()X)2.`3,?W3%I:[5)&-R@,8;G890.:"[ +M`]TXUQC>3E-SEKI*`FQHQCB>R>(&#N4)V`[CE.X>GCY"!VCU2,]D[@#.\I@) +M`EQ(`[IVF!/*(36_U"!",@:-6!/"C:7-.F)&$[LY&P/*`P&^V=RM"P8YSF@3 +M/8+.8'<NTQ^JT.CG_P!2UI."X82CZ5_`*Q90Z!0)`EPF3]%Z]8M](`_1>7_@ +MY58WHMLT<,'[!>G=/K#2#.ZY95QQC2HXW^ZN4F8&RIT'<\*U3<(X6'18IM$[ +M^RLT0`/T56F9((^JMTHP9V14S8#"XCZ*"I5/F1SW4I&(!4+J<&82K$U"H8,G +M,IJK_>4-,:<G)2JQ'*""Y<2/U5&\`@DX/"N5G2=\CLJ%T0>0%#3A_'#'AP<) +MB<X7"=>N"2&,)'<SLO5O$5@RO0=.>5YSXDZ.ZD7/:V2"N.?B]NFL<],FI;U/ +MX)I+IGNJ@K,MGR-QE3BI</\`Y.DG^RT_#_ANO>W#7U&G3,Y7*>'*_&O:`Z1; +M7?5*S1I(9O\`*[OH_2&6UJT:1,=E>Z#T2C96[0&B5IOIZ6B&X7HP\4Q8N5K/ +M99TV@@`;3LH*U%C7:8"TVTGF1D(769).)*ZQBLKR#'ID\J7R/3^671@+4IV9 +MQNK%*P<2"6R5H8M&T=@Z4?\`#/#IC!X"Z>\\L'LI!T\NC"I(YW^#($[J6C +M;3(^JZ.GTX!OY0C;TYK0(:$-.=_@L;;H76T#9=2;`%L`2H*G3`'9"&G/?PX: +MV0W"C-/3,#]UT3NG']5!4Z:X@]BJ:8+J<_EYW1A@B#QPM&K8.!(TE!_!G)VQ +ME$9KZ0&PW0NH-+<P!W5^I0]6)P@J48;M'LJC/K46Z?3RJOD>K:%JBF9(C=15 +MJ0DGE!F/HASH.QY*AK6D[#9:3:<.)_=1U</CCE$8%?I_J=$2JS[)[28;(!70 +MUJ37"0`JU1C6B9E7;.F!<4R`0YIVV*HUJ#@[6`8[!=%<V[*GJPJEQ;`43!QV +M*2&W.UVDN`@]U5N*`J>E[9"V:E"23'W52K3@D!NRHX[Q'X9MKT.+&!K_`&Y7 +MGWB?PO<V)<X,);G9>TU6%C28A9E[:T;MQ:^#\J\7MO#RY8=/G6_MWTZQU@@[ +M0JO'8KU[QQX.I50^M;MAR\RZSTRXL:[A49!G'LM7'4W&YY)E6<W!B)([A/,N +MV&G9/4`D3QOW2C.28696M'@@',X[*6W!U`#G*!L!HQ[94M`-<2X"(*J::--\ +M4P"V?<!%K'^`_8(:#:?E-U.@QME'II?X_P!U/:&J"Y]+BT&`<$0J56)G<<1* +MMW3?ZI$\A57B=GP)R>RM`U6T&V;'MJEU5SB',TX`[RH!^;=$\^K!!08=.8(X +M[J!-;_U2.R$M<)D0>Z=I@YPED`D.SW)0"]I(DB?JK+`*-E4(>TOK```;CNJY +M+B28VY3.RV!QPH&J/U2`C,'X28<29^Z?7F0<'NJ#IR':Q,QC.R%P).`#/ +M,H28.TH@2'9.-E#9Z>VYE)XW!;/:$MAV0ZR3/?94/Q[)#3W^H0Y<#`VWA,"# +M+H(*`G279PEI,;[YQE,2!DRBD:9S\!`+MH/Z(28<"6Z@/U1-TCN/=-5<=69A +M-*$'?LB)<,SN(3.(@P)0D@`X/RH"DY,<)'CW*8G,?=(.,Q&P0.)C3LG)(,-. +M=Y".JRFV@VHVL'$[MY"BU:=\]E=!W%SO<C<I,&#,^T'E,'N:)82)WSNGU8D? +MHH&,9$9E,Z!RG+R#IS'N83AP@;#W"!-D-Y^919T%QVVSRF!&Z(N;Y<;'5R@$ +M$`R/T1``N,3!V3M>TM(."$@0?A`+H!@S\H2'''[HR3IC!^0F(@F?M*H;3F2Z +M`4OS2(!]TX(CB1PD`.,=RH%`$22DV(Y/LE@G/T3C3&((F,HAVB"1]$M#MHG2 +M<PFV(`'W3@D2/\7NBGDHBXEA:W8J-Q),$B>(1@P,X]Y0)N/G96^G/+:@)G&% +M4P3),@^ZFH%K8,Q"#Z&_!#K!J=.I4M660-U[3T>L'4FS\[KYE_`GJM*C>>2Y +MP&H[DKZ&Z-=--%I:<'LN.3EU77V]00.)X4VO.)PLBQN6N$3^JTK7U#<P?=9L +M:B_;N)*NT2-,;*C1@*Y2(,2BI2Z,)4W3(*9S03C*8PTS$2BI'1&^.ZBK/$0A +M?&C_`#4);//V2@*ASA0NIR=1W5P4H;LF%'!(4T,R\I![(E<]U?IU.H3S*ZNZ +M8!L#*RZU#SGD?LJECDK+P_1_BB[2")76=*Z73HT!I8!"NVG3PUH<0M&WM]+` +M%I-*3+8Z1B$S[<G@+5\K`'9*I18UA)5TK,%J`)`"E9:@P3"FJUJ5-N7"/E9_ +M4.M6U!I'F-GY6+G(U,;6C3HTPV3IPG#Z+($@#W7*7OBBBUITU6_=8=YXO9KT +MBKGB2N67GDZ=,?%:]&JWM%HPYJB'4:6K\X`7F%UXO#&F:F!S*JT?&]`U-)JM +M!^5SOGKI/"]=;U&F'?F&R1ZBT\[+SFS\34*K`?-!GW5ZVZY3?4TBJ%)Y:U^U +M'=4^IM$Y4M._:\#(*Y&A>^9EKMU8%RYH_,M3R5+XXZS^)I.;^Z<5J+O3@2N7 +MI7;MR25)_%/U#2\_"W/-6?VG2>73?S]$#K)AF`/HL2EU"HSG;E6:/5SR2MSS +M?EB^);K=/;I,#/95:UA(,"`K5/J5-[<D%6:=U1>W,`+I/)*YWQU@/LBV7053 +MN:'(;E=:ZE1>"1"K5NGTG">%N65BXZ<DZWAIE5JM$3!P%TUS8$$@`1*HU;`@ +MDQNJS8Q:E!@;@'OV5"XI>HY,!;=:W<#$3"JU[:0=059L8D!KLY"CN*;7D#A: +M56VD$8^57\HM.1*(RZ]!H!`;"H5:$/)B%NW%$ETZ57N:`TR1*K+G[ND"TF-E +MEUK5S27[+JW6[<N(A4KVW#@0UL2J.9K4V/I$/R2N4\6^%;?J+7/@:O;_`+KO +M+BV:#D2JE:BSZ+4RLZ37U\\>).B5;"Z>W3(G!A8P8`2"(CB%[_XEZ%:7])S0 +MP!W=>7>,?"E6SJN?2G3RKZR\QVQ\F^*Y-K00!.VZFI?F&<IKBD*3S3$R.4=$ +MC!,++HOT0/*$X/RBTCO^J:B!Y0D@(]+?\014%T&C())[*I4(U06P1A7K@`O, +MP>-U4>X$1@-X'*"#3/J@X0.QQ,J4O@$MVV(0.#0!ZM]S*B(R!$@E"8(C@*1[ +MB7N>3E,6^D.<TP=O=!&WW._NGJ=VDM`2[XF$CF)CX4-!!C`._='2@RV(Q,RF +M=$3&W"9F&QGZ\*FC@$?U$GV2<8Q()*4AVY2T@$,@^Y0-)!S'NDT-G'.R*8<9 +M2&F`#@\J&@M`,F4QPX@[*2&[@?5,6^@F!.T*P1N.<9E$T`,F8C=#IAW^PC:& +MQ`P.5%)@`IZI;OM.4!GC3D=D6`US8,[C*3P'8;P%0VG`C'LF+27]D36X#G3G +MDIBX!T@@>Z`8&"4T0=_UW1%H.9)!0N@$_M*@:8WRGUY@?J4MX'*3&M[Q_=#1 +M$G3,QW1%Q&<Q\I@T;;$^Z3`Z8,GX0T3A@8X[ICW('Q*DTAV=1S[IJC0(TN(^ +MJIH(/$@)'8NW]TX``R0$HD&<B>ZAHFX.-N?=.TY'LD1O'";\L'<E4&(.Z;)D +M$(1)!S]TH.H;GW"&CD`9V1..8'VWE,&EQWCY38D0=T!8+0`(*:/3L8Y1.:9R +M1MRF`@YGW(4".!L430]L5`"T;C";?Y&Z1F2V20$31.C5(3.,-`2`=&^Z3PW2 +M,S[*KHPTC(A24JFDZH!`X,J(F#QA.-LF1[%#3;\+=2J=.ZC2K4GQ!V[+Z2_# +MOQ'1O>ETWBL"Z,B>?NOE<&#.K]5U7@OQ;>='J-#3%/V)/]USSQ^Q+CM]8]+Z +MB-7YC]UU/2:XJ-!#XYW7A7@?QG;=0H-+ZH;4Y;,2?NO1/#O7FD@&JT_!7&\, +MZL[>E47M#<E6:-1O*Y.AUIAAKB!*T;?J+7'+@1\J;5T+7MYA!7JR-A[K/H7` +M<?S?53ZQO*JI6^KW"GI4S@`_HH6.;N/NIJ=3L1CE6"4-#1.?A,UYIG5@SB"$ +M+W@'?=0U:K3/L@JW>7$CE*RMM3M1PC#=1WGV5RR8`?=6(L4[66`A05'LIDR1 +MA6ZUS3I4"=0PN"\:^(_X?4VD<[83+.2+CCMTU[U:WH-)-1N/=<OXB\;VU&D6 +M4WR1OE>:]>\0]3JEVESH/RL!]:[N'ES]><%>3/RVN^/CCM.K>.JKI#7&/E<C +MU[Q;?.)<"\@^_P#JH6]+KOD^HS&\JQ0Z`^JPAU,GY"QWVZS4857Q5?UC'K`F +M)@PJUQU#J3G"H*CBWVE;=;PHYM:(.G?'?[+8Z5T6EH%-S,C@C">L=/:3IA]) +M=7O:!;5)SC.ZSO$/2KRE4-6DXP.R[K_@[:%0.:W3_97*G2V5Z&DM!/=)-,^U +MEW'GO1+R^H/%-Y,#NM^EU&XIM\S45?J=":VM(;'O""KTPAT:#'"Q8U[;;'AS +MKS]3653@>ZZZROJ==DM(7GM&P?3:'#$+6Z76K4:@:7'"2V+K;M#5WAREIO?/ +ML%C65Q,:C'NM>@6%HV^JU*FA.JNU0)1M>X['9`Z"TY^B!WY/VRM,Z6F5W-Q) +M@X4S+EXR'&5EE\#>5(VIZ<.A39ILTNIU6`2?U5ZWZN',+7+FJ;R2<IV/(Q*W +M,[&,L)75"]HU!N/?*E#:-5F'#X7)&X<UI+7J2VZI4IF!4)"ZX^9ROA=#=6-/ +MR]0`^RS;FQS$?9%;=8U,`<<*V+RG5;!+9]UWQ\LKCEX[LBT&`9E4;FV>! +M,976NI4ZC<*C=V6#NNDRCE<7*.:0XR,J"Y@;-"W[BQ.LDM69=6NX`"TSID5Y +MF(4+V@L@MB5JOM"&203"IU:0!C9&6'?48.V%0N;.6X"WZX(&P@\JG6AWHB6K +M6T<M<T"'D`'[RLKJW3_XBD]I8#/LNON[7/RJ-S2TMTZ?N%>D[>&>-^@5:-R: +ME*GCF%RX9I?!!@<+WCQ'TUMQ:U&.`)(PO)O%/1JEE<NFG@[0MV;YCKX\_E5* +M#6FDTZ@/NBTM_P`8_5-0I12`!'V1^6>X^RY:CLJ.KUJ;*U*G5=396:&5`#AP +MD'/V_14GP#`D_P":M7@;JF/]54J#B=\[JT`2UI`G_)';5*#!4%>FY^K8M,05 +M&R`PB.VR$.]4Q*E@,TF/>UM$N<7;`[J*."XHW!NJ0"UO!E"8(`.4`#Y^DIL" +M)._;*.(.ES3(.W9,6MWDQ\H:"[V)'PD08TS(Y3@`S&!W"1`+<X*0"R2[)/NB +MDAPW3M#=^R48VGX0`?9R=X#<@G/&Z=S00(&)A._3P#@(!#3IDN$?*>2)D[;) +M@V#G>82!D<_912!@Y.`G=$C.`F($@$004[P)W]T`@2[*=QQ)^GRGB`29^(W3 +M:0002<?1`Q@QI^D).!F>/V3F0()(0N_,1G/'=`+B8B1',)LGW[)W?_<?IV2F +M'#)309H=].R=I=M.#E.=.G=Q[(0V02#LBGU.+B<92$@?(W2S`D1"8S.#QL$! +ML+IQWV3DQ$'9"P@-S)^41`P=39.,<(@9DR7#.8E2-UAD84;0-S@#V3M,#)D` +MH#+O2&D9]E&XSB-S]D;3#?S2#&$X8"T\CN@`.],N'W3M_/+A',IRTR`8&R8, +MW`+23PKHV>'9CG.R$.,SCX"-K8P3OCE!'IG[[J`B1(C8[2G$EI`'ND9D;93& +M9S$H'GU&!`A,XNS`PC:"3$A`X<M."J!U9^46LZHC9!._^:(&<#/N5)`+7'5L +M"3NC)`@X^J%\AP(B-L=D37<P,"/9-`B_@@83^81$8/*9NH@_?=-,CO[`HK3Z +M9U2ZLG--*L\'V)7;>#/Q%NK2X:+EY<WN2??W7F[2Z03@GA2,G@F#NLW"9=KM +M],=#\?6%^&!MP&NC8F%V?1>LFI3:X5<?*^0+2]JT'!U.J]KA/*[3PQ^)'4.G +MT/*J.+QP7%<KX[&;C^'U3:]9VE^?E:_3NK-JL$/E?,O2?Q4\VMIJ@M;R05W_ +M`(8\=V):T^<,C8X7/5G998]OHWI(&=E,V[;OK`(W"\UH>.>G>2)KM/O*+_SI +M;&`VN"1C&?[)N4>B5[T-F'?""A<^>,.,''RN/Z1U5_4BT,G2>2NOZ=3;2I#6 +MMR;9VNL>6'\TC]D]UU"G:T2XN&%5N:K:=-Q)^ZX[Q7U-Y8ZFUT`*97U:DVF\ +M4>,0POITWKC']5=?57>8=S*C?:/NJ^27%:%ET.'MAL^Z\F6[7IQD@;>QIW#- +M6G)Y5BUZ'1G\@QG9;G3^G!M/0=BM.C9L:P`"/E6+_P`<]2Z12`/IB.5<M.FL +M;_\`HP/HM@46@&0(_=)K6MSP4Z)&1<=+80?2"=YA5Z73-+I#1[0%T+FA^^_9 +M'3MVZ.,)VO3FZUI(+7MDA/0H:29&!PN@KV;8,#V*A;9@G9+"5AU+9K7!V,;J +M,VS702``#PMNXLQH=ME56VWKF)E9K49-Q:CRS`_U06-LUY$B2MPV8>'$]E5M +M[44JF<05&H@T.:T@#A:/3ZCO*#3NBI6S2Z2%*VV,^D?"B[2M,C8?=([R%+3I +MP())"*E193:0R0)P.RK*HYA)QO\`LH7/(,*[59I$JI787.V1=&I5#\(C5]4! +M0GTDD[A1O=I((,PIH6WU!I$#=0.<"R1(^%%K+G`26I`D:H)DY24T-]9S8TJ> +MC=O8R0X[\*EL3G!4-2L6OC/PM)9MTEEU8C#C';*U[:_H5J8@B8R9W7$4WDLP +M[Y4E"[J4LASH^5TQ\ECCEXY7;U&4ZK2&@+.N[`#(;NLNSZN6.ASX[Y6O;]1I +M7#1Z@O3CY97GR\5C(N:)8XM((E9EQ;.+R<P5UC[9E:7`A4;RP@#&/E=Y7#+% +MRMU0=I/99E9NA\1GA=1>VS@8`$+,OK?09Y"U&+-,>H/3J+=/UE4KVF'QZ<E: +M-U3<7%NK2!^J@K,I@8=!"LX1SG4K,E\'DRN;\4])IW=B6%@<Z-NR[CJ%'4#I +M[\+*N;4#&_<]U9=7;.GD%3P_58\M;L#W3?\``:W;]5Z?4Z30>\N(R>Z;_@]O +MV"W_`!=?W*\#NZD/APSR=U4JOAT@8XY4U?\`.2,859PAW8>Y7.NVR+H.TB,% +M,7##HC'>0FQ$B$S@`T=SQMA0V,U6;-!@C8H?,TD.8=MI0%H`F9A`(,AT^R"5 +M]8OJE[R2YYDDF24.L3JSGNA@:9!Q/^PE&(S!W0V(5#N`2"93O>#F<GD(/3R= +MT.`"V-C\0AM*7@MD[[$RG;4&PD:0HP`>8]O9+$S.P022,DG!2#O1$X''NHY` +M[YV2:(S.>Z`@0'0-MLIVN9D\;*,@S,XC9-`!DN@'A%'Z9U<#$HG.86CG/*BW +M><E'JW`.3RB;.'-V=(/()V17#6MKEK*S*FQU-F-O<!009,0$HD';Y)1=I:=1 +MC7`U`'CELD<>R'6W0TM.1PHL@`0(&Y3`8,9A#:1SFAN\CO*4LC\V3R@CD8"1 +M&<X@("V$1CV*)IQ!A1Q)DGA(-,?JIHV,/;J.8!3ES"Z3IGVQ"`M(WV]D)C)G +MX"NC:1I;F#PD).9;@[J(Z@8//9.<`'ME!(UP@B`>9*(P&@%TQNHH@9VYRDT2 +M,J&TNKT_F&/T14:C09G'90`&,?;NFC.VWNJ;62[UR#(D)FS&#C]E"&.B8._& +M82(AOPH;3D`#?9"/SQ,R@9L3,':"A&"8P=O=4VL?TB=C[)B?4,S.)4+2YVD@ +MP!LE,;*&UAI$!TB/=`XC!^ZB'IP23W3.RT#GE71M)N![)X'$Y*A`(?F8GNG! +M(GV]T5,WL.$F<-])A0DGO\P48,N:9G"(E#23,_24X+2!`CVC"BUG,DMQPFU8 +MR0"-LY1=IZ<9''S!2:2)$P.Q4<@/W,<Y3:SH&=_=#:>3K&))X3AQ)D;;C*AI +MN(V.0B$ZMX]DT;3-J.;D3@YA7+?J%RT'2\YB<K.:2V"#F.Z*B20-.2EY-MBA +MU;J#RUIN:AT["=EZ3^%5AU7J-VRK4<[R1./NN9_#SPA5OZK:U8$L]QNOH7\. +M^@LLZ%.*8@#Z+%D[9S\GR.O\%]*9:V;!O`75@-#!(@0J73J8I4?8+.\3=6;; +M,--A`G`6,LM,XQ7\5=6;1I%C=SV*X[1<WUQ.8[*6GYW4+HO<XQVA;=E092I; +M+R9WV[>G&:!TKIS*(!?$K5H46X`&%%2:YYX4]'7OL>5C;K(MT6L`G2G>0'"% +M$R3N8Y1!CC`F5-[;D.XZA&`0F;3^P1BF=)[J;R@Y@/[J=KPA\OG,*Q1;!C>$ +M@SB"/HC8P$AORKK25)I:3D`=_9(6[=)@#Z)B0-A/"-KQMA:E9TJ5:68CY4#[ +M?2"8RM%PE^P3:0Z,*5J*`I13_=4G4?YA@8'=;GDRT[2JXMP2#A9UPUM4MZ`@ +M$_JIJ5-IF0K#J):.WU0-9$08/92B(,=JP,!3-I_RSW4]&F3EQ^JE%.7#N4D& +M959B'#!Q"J7-*#$;=EM5J30W;*HUZ4O@<;H,FNQT$D0J[R)C<[+3NZ3B-L+, +MK-WWQRHL!4R#$9[)A4+1,92;)$&$&\JE-4<">Q]TWE!XF2/JD]P!@CZH758: +M2W*=()K0UF#,IY;$"%!YH&"=NZ'S@';_`$2&DSAWW1T*]2B):2(XE1MJ@GF4 +M;(>V"5N,6-CIO6"QH8]Q$]EN4;FE7HX>,KAZC8!(,?W3V74*U$^EQ@<%=</) +M<7'/QRNMNK9NIVD2LN_M"701]5-TKJ].M#:KOKW6MY5*NS4UPSLO5AY)D\F? +MCL<1?VV@06K(J4'/J3)#5VW4["23I,'E8E]9.`)&'+MVXN<J4X>03C*KW%LW +M20&_5:5S;Z'Z@JM5P+M)V"#--"F#!R4WD4NQ6LRC1+`2""47D4?=.%?)%<D/ +M,#.T`JN]LNQL=@58?!=),2>56?O.XXRH])J@@$.TRWLHV@@',GA&]QYR.4SO +MRDR,<(R"1)`W^4B(V!2G>&IB3JVX[HIW:-1`.`<80.B9!(([IYDP2)[IB2)Q +M]D#.=/(^Z)H@:L'ZH3&G#3O\)<B<E`39:,Y]QPG#1#G2,8B<E"UY`<8$GLB9 +MP"WXA`L-,&9&$],-Y=$<QND0X$AS((.QX38@J@2#N)W1-:-.<#ND#C;"=VG2 +M1[;J`,D\!(F6$&2!P4X+9&K,_1"XM#L9212@=I([)J@.K?YY2$1L?ND70TB, +M)$,0W`)VQA,&[P41(G/ZI-B,(I@0`1PGDG)^$SC!$<]D\CB90.T'8'<[(23_ +M`$[%/J[DQNGQO*!G<9"3M)$_W2!@2'1W"0@\?K"!:6YQ$\)I@<_*)Q]&YC@' +M9,=(=D?J@1$B!GNF:!IWA.(!F<)0W,2F@B!&9RA!&F#PC^<I/T%H`$)H,PP? +M?E."9W28!`$P">R8B#G]$"=J(_U3M$X;B>$YD`MF0=QW3-@C+9(Y0%I;K)VC +M8=T-0R[_`%3AH<[N!PF>-),B/8H'8).9,(!IF/;!)1$ZL>R$B,<]T#@0P'?@ +MI@9/Z).,-B4FG&V_*!P`1C/LD-0!]1CV3,.(`^,HFF),8^4#C5N/L$[9/IC, +MY(2$@8Q^J*1(:1]$"(],0<<)J>F-DY@M$?ND(F0J&/I<5+3(,3D$IG!I!.QV +MA.&P-ON4"(!$#E=A^'7AQW4;MKZC)8TA8/0.FOO[UC`PP2O>?PZ\/FTM6`L' +MRLWIG+/UZ=#X.Z*RA2IT*;,-'*]+Z);MH4`-`"R?#5@U@!(B-PNB>6TZ>-EB +MN<YY1=5Z@*%%P:1C<RN)ZC5J]1O8W"TO$UWG0WE0])HAPUN$GW7#.N^$VNV- +M'RJ0B)A7K>F7#U)K2F2-ONKU"G!CLO/>7IQA6S`QHA3&F0"4@`'2<*1[FAN( +M6:Z2$P"6XP5,T#&%`Q[2_)_56&'MNIMK22FUNG_52M'HQRH&$ZO2K5`#3_=6 +M4T`,,0!",LT@3^BD$.F,)G./*K*%T\H28B``GJG_`*E&7`[S`02XB)1-B9C" +MB8X1,DX1TG09/*FQ/'I@#Y3^6,N)@CV3-)(D(Z?YC^J*!](O?`0U:`;D?16& +M-&8'Z)ZE)KP`X2.45'08W1&".Y5@4QH,)4F'4('W4^D:)'*1*I5*0!DSE5*U +M(:C&Q6C6:!.)A5JC6S);E18S*U"096?=6S1_3!6[7``^50K,#R3C"G2L-]') +M)&`@\HP?9;#K=A&!@JLZD!(`0K(?3)!&WNJ]1C]/I(^@6S5H`B`,JM_#:21^ +MB:1EO;IG!*A#'ZCO'*TW6WJ,A)M!H$&$TNU&DPG?CW5BG3@;[J846L),<HF: +M8$Q\*LVH'M.QS`55WI<9'U6D8C>)5:O2#G;;K4K"FUY:[4Q\%;/1.LN8[RWN +MU`+*=;$B<J&M1<W:<96ION,927MZ'9W%"\H2""85#JMB8)CX7,]%ZL^V>&/< +M1_OX7765[3O*(@CW*]/B\ORO+Y/%KIR74K0LEQ'ZK%K6O\S6Z3"[OK/3@6DM +MV7-=4M2*1TSCL%ZIR\MX9#7M`B?U2\QO<?=,*)Y!E/Y/S]TTO#Y%K-)F71]5 +M6>0<`!6*YEQ;`&..56>1Q"E>H+@2/8)/!D2-TSMA)QV2(F'$X]D-&<,X[(2) +M))&41`'9"X]T#$&9@&=BD9`B"2$PSMQPB<US1+AID2)Y1`$&1.Z1!X!!/&R= +MND9<X^R8D2/[HIP)]61"(2.#'>4)@#_1%3=+80/+MR23WE-LTZ@DZ8&04TX@ +ME$.!Z9S!2W[I9#1.4I+M.?:.Z`?3S,<)`$DX=/`2AP<<X2/<[#;**>J^GJ;H +MU;>K.Y00TGL=DY!C4[Z2D3)D'/NA#`@..GM&R0)`^>R=I+<S![)R22),H&@` +M3E-B!E.23DY]TG0.?H@89=CZ!/.,9[I@Z,#*0)$^Z+H[-B8D]PD<MS'=-,@1 +M]@G;OB"B%@'\T=DQ[!%D[QGB4P_(80-&8.",(C@;`I-D@`&9X2&\0#V*!MAE +M-N,\\HX)'!.^R$N&G;/[(:.&XX,?JF(,SLD)&0TY"0$MQQNH:.#B(&4BT<$' +MW3@B-Y`V"0#]<M$&,C*!`#<\]D\M%0.C4`>4T>F"W=)L!ID?14/=.I.JEU*G +MH:=FZIA1N]@B#26ET8:)WV0F-!Q]5`Y!<=1&!RF&Y@>^Z0(E.8!$85":V3D' +M"<`:<#;=+4T99(/)*=H$=^Z!"1$#*<09F?HD1MRG='(DSW0T8>SI]D]-OJ@D +M;[)@,;F?E&(&^_LBB.6P>58M[>I7<UDD@;*!NDD9,_J5VGX?]#JWMPTFF0T' +ME&;Q'5_A/X:T-;6JLDD"%[?X8Z;%`'1"Q/`_164[:FP#``7H/3:#*-'3V6<J +MXR;NTEK1--L]O95.LWS:--T.B%-?W+*-)V8A<AU2Z?=7&ACC!*Y99:=,<=GH +MNJ7=QK<XD$KH>F6^&B%3Z):!K!A;MM3T`8^<KS97;TX8Z3T:(:,2`IF-QG9" +MT@-P<!4NJ7].WIDS"Q:[1+U"Z;2!,C"R:O7*4D!T%9?5;]URYS:<Y[*G1Z/7 +MN8<7EL]ESN[>'3<C:/6FR2Q\QV5RRZR'D`N+21W6=:>'SI:,GW5D=%J-R&N! +M"OK4]HWK6[-5FH'Z`JRR^+?S#3[K!L35M7!KP8^%J4GT[AHV"FMK&A0O&N/Y +ME-4K!W]06-68^B8&J!V34;PZH.RSNSMJS?34>Z7%`V=63`4=&J'"094K3Z@1 +M&5N7;-@R('I&ZDH@ALB$PRTG]$3()&(5TSM-3P8S"GIM`,A1,(.5*#VQW32[ +M6*8!@2BTMG?.TJ&B1JVE66%IW"!1`P[[(RW!F4@0`)&/E-J&<H(J[/JH'4X^ +MJLN+2TF=N`JU1P#8,E18JW32-C*KADF"%;())2;2C?=.U4JC-(5=U*72X0M% +M]+42!.%%5HC/9$9M6F&["0JM5H`)G*T+EI$P-N2J5<8/?E39I1JN]6X5.ZK% +MA)/"N7`QJB#^ZS+MI<W)(!X4BZ05>IX(:[([JG5ZH8!UPH;\-9(:!/*R+IQ) +M@'*9$TZ&WZDW5)=,JT.HTW-`7$UKEU/)<<=E2NNN/I,($^V84F5B7';T,WK# +MC4/;*FIUJ+VP0"?E>3?^9;IE3,Z?=:?2O&#&U`*U32??*U,_\2^*NZOZ/]30 +M1_=6>@=0=0KBF2L;IO7;6ZIC2Z9&_P#L*=[VEVMI'>97693+IQN/RO1[6LV[ +MMPT1)&95'J=DT,((^BQO"G4SK:U[L]UV+A1N+4.!X7J\7DWQ7C\OCTXBK9-% +M0Y_1-_!-[_HNCJ6)+S#"A_@3_@*].W#3X'N-RZ8G"KDP2"#&RL5\F&S([*"I +MZJ8@F1B%-O2A._<#W2.H#V3NB3N1&2FJ.);OM$?"J<!P9@_KNF<"<[9A.6Q@ +MQ*8YQLHIO[[IG;03/U2(W&>R:">3\J!#,G*7S(A-M[)\=\<D(',1DR`8R433 +MG]X*`C)!G='@;<JAIGCZ%)P'"1'SC=$/RSNG8$D=\#(3">Z=P:!D^^$HD2=O +MV0)F#R)3NB,QGNDX``9'U"1'I,<(IA^4$B>Z9\``C]D_],S!A*0/^H>R`(SB +M<>R=@)G.Z3@``Z1GA,#/$(%IC'"49!V^4XR)/"3))$9[94"(R2(3/V_=&0#. +M-]\JUTQ_3J=.Y%^RJ\NI$4?+,0^1!/M$JBG!C<=HW3"=6#D(FDZI`GZIRZ6P +M2/D;H$"TN`<(2<,P)^$((@#;Y*=TD8?C_P"Y`\C3_ACLG:X@[S*;$$S@=RFU +M9E`1)X.=D)!C"67&>P3`8,9*!`^J28'<<)R0-G<]LIB!/")C27`-&78B$"&/ +M43@IP#!,'2>3RF<V)U-@_97.FCS[6K;%S6@`U6$[D@9'U'[(*U3`$['OA"<X +MQE.-,R,SRD[3J@;(`,C_`"3MB/5^Z3@#!VA,&R#E#9`Z3@;B-DWJGG"+9I2_ +MJS]4`QB3RB9&9F/V1%HD!IG*<-CT.G4TY:$"IZ=>DR6]P8,IB0#I&8[%.6Z9 +M;I(/,H?S/COR25%$QQ+H(1F3&G[E-0\LEQ<7#'')4]M3+W-`R51?\.]/?>7; +M`&D@D?NO>_PS\-MHVE*:?J@25QOX.^'75`*[Z4S!R/A>[>%NG-H4VC3&%,KI +MPROM=-+HMCY%)N`"KUU<BF,$2G>YE.F-6(7.^(;\'4QA@^Q7*UJ17\0=1=5< +MZFUQ2Z#8N)#WY)]]E3Z=;FXN`]V5U%E1;3HC'U7FRRV]&&.EJW8&L;Z=E:%0 +M!NX5%U9K6Q(V[IA4<YN(*Y6N\BS>WK*=(Y7-7?G]0N-+7'3^RT;MCZSX.%H] +M.LZ5"V#C^8Y6=;:WIF].Z2*;AJ))706%FP``-5>B[75@;<+5MW,ITY<8"WC' +M/+)<M+1@:!I&%:9:4Y@M6%?^(;6U<9J"1[K/_P#.M`5L5!VR5O<CENNHN^D4 +MJK(T`#NLVXZ54MI<P$CV5OH/B"VNP/6"3[KH&TZ5Q0($%/69-XYV.1I14ECQ +MD8(*I]0LG4W:V;>RZ+J?3#2JZV<=E5%)E1A:=QW7++'7%=\<M\QA6M5[:D1A +M:5JX&">56NK1U*H2V?E-;EP,+E.'2S;7ID%L0$MLZH"BH$>7JG=-4>5U[<UJ +MD\'=RFU`"`LRG4@R2K+*X(W51=IU(;&I3MJ@;'99@K09+AE%4N1$"")R5EIH +MBI()F$+GQN539<0!D3\IZERV03G"HLNKL&^ZA<\..3E47U'.,CNIK9WJTJ-K +M=-@+D]:0?9-3<!F$3G`CU&%649<`8]MU#7>W3A'4(+L9&RC>!&<H:4KD@M)A +M9U8')C&T+3JL:X$!5GT@UL&,*:V,VJWDK.OFAQ(;_HM:Y87/V5.M1(.RD@Q* +MED'DR<'.5G7UK;TY[KHZU%P8=(63=60?5.K.54<U>6C"XN#C]`L3K-E+"0S8 +MKN+BP@2!C9974NGZV$'93U-V/,>LNJT7END$#W61<U*A)>:;@!R.3\+TOJ72 +MNF^0T>5_,`AQX/NN3\06MM3)T,<Z<XG"W+)PW[[Z9W0.O5K9[6ZX`&>?[+OO +M#GB)MT/++B';1_L+R/JE-S'DM'I.T!2^'>LU;&Z`.8Q!6KX_N+%LO%?072KK +M14:X;E=]X6OQ5IM#SF(7C'@[KM.\HL!/JB8/_9=]X=O13<TAVZ8WZ\_DQ^5Z +M0&L(G4,I:&?XPJ5I=TS;,/<*3^)IKU?NO+^T_/.Y=+W%I$959T`[J>M(,0(V +MF=U7=(?&5Z;VTC<09`XY2JX`;!VS*?&L`NB3DH.=\;)0F@D3G?=,\.!S@^_* +M:21OE(N)(!=/RH&R7<;)`ZN3D<!.R"9G,[(<;YP@6)D%WLD#@$.^Z4`F9,>P +MA(#$?N@/`<07`\2G<X08V*%O],IW#TB!'=%(D3(Q.Z>1G.Q0_P!'O.<HL:=4 +M(AB)_JSV2,%H2<?Y@`A.0)'$A(I@.Y3F.#.,IB0#B/V2`)J$;#]40Q;G)^XW +M2[B=TAI+^XXRFIP,@(&(EV_Y<)&0)`P$9PS!R3&$!)P3(A%+$23]$[9QF`F# +M)P!A(@8$P@)O?^Z1:"TQ`)Q\IRUHI?F:3.R%X&D\QE`Q&./NFGD1"<`D8!A, +M,G,Q[H'/R.Q3'!DQCE,X;QC,)VM$-<1,^_9`61C`QW2:(!.#P@(B!C[IP1.T +MSB$!.D-$;I-QP2F@:=X]DXC7^9#9$`),:"=($SPF@2<(@&Z@9QR4$A?-'0Z( +M&=1&4?3'`7U&HX2UC@YPC@&5"XR,$P[V3#?'&$!W`FK);OD#="-QC<X1!V`3 +MVB24S@=/^J)L,08YC!"4278,@(B#(CD(2,X/**8`8P8W3D8[X2`W))@F,IRX +MG$J!?TB!SV2$@@B3';E/IDF)CB4;6PW/.RH0))DG+M\IH:1DF`4AJ)B2`?=. +MT$@-YE`@PDMW(_5=E^'/AYW4;YA<UT!P,?9<_P!$Z=6O+EK&MC(SE>^_A%X: +M%K:L?49ZW9_92UC.ZXCJ_`'0:=G0IL;3#0!L%VU.FV@WM"@Z?0IV](#:`J?6 +M^I-8TZ'?<KEEDSC#=;ZB&^EIR0>5SH=4N:Y,[G=!7J5;FK(G=:G2K4,:"3\K +MSYY;>C#!;Z72\ILN&%>KW(IT\QLJP(`+01E*C1-5^J?NN5=I(9M1]5\SOE7: +M;M-,9(,*2VLQL&G.ZLUK2&&!!C*QITE9U2JXU>ZN![BP1C$8*J_P[F5@8WV* +MMTQI;D9(315RQ9Z0X1C<]UC^-.M&RM##@#NMFU>T4R-B!"\]_%-Y<2!\_.ZZ +MXS;SY\./\5>,',8Y[ZPF<"?E<A7\>5_-.ET@']%E>/7/IW08`=)Q^Z[C\"_P +MUZ'XDZ"[JW7*]8L-7RV4Z#@#L#)D>Z[SQX_6-^LW6A^'/CJJ^JP&J02=I^%] +M#?AYUD7UJPN?F!SLOE?QIX9'@_\`$3^`Z=4?4MG$/ID_FTDX!@;KW7\%;ISP +MUAF,?V7++#URX:W+'KMS2;5I;;KG[^D;:L2``"9E=-2@T&&(D+*ZU1:^FXM& +M96?)-QT\=U6-69Y]/4(E4*E`M>8^JNL?Y;P#SA2.HM>=0V.5P[>GI3HU#^7? +MV4E:"W/'*CKTS3<2!`05*X#(..Y5G"6`,->/A.ZH&[''RJU>LT2951]R"V`X +M+>V%]]UIP24A>-`!!SV60^L7$29E.R9P%A6Q_&`DP2)2_BIP=OE9;6NB#REJ +M+9.P_=%;3:NJ>ZFMG'5JGZ+$IW`!''.ZNVESD'ZJ+MO4R-`DH75`2J(NP6[I +MZ58ND?JKM(MM<&NB=T-;).1'LHR^`)2\S8?9:A2:UN3LH:C2YY4KW`@D'/RH +MWNS@_P"BJ*U2B`<M'OE0OHM/?93O?ZB21'LHO,SW4%2K1&F(E5&VC"XD[RM- +MSVZ5`ZF0XN_I(51FW5NT>ENRR.IV9+3S/"Z&HTZHX56XI!P((B$B;</U&Q=4 +MEI;@[K'ZGT2CY)<\Z3W.5WMY:NF0W?*P.N=.JUIW'NKZRL[>:=<Z9T]C=)R9 +MSNN1ZI94Z3M=,F"3$<+U>\\.VOJJ5@7.))DSNN;\3=`M?))INTD;2%<<KCVW +M;*PO`?539W+070,"%[%X=Z@*K&%KP9@@KPBO:NM;LN#@(,2O1OPYZBY[`VH[ +M((&-E<YJ^T3*;CVNQOHM*8UC92_QW_6%SMK=4A;L!)F.ZD_BJ7<_=.7+A\97 +M4$G<?[[*!PEA=!`&)!V5BX=AV?;Z*H\DN&,;A?1KA0/$R1A#48=$R43_`%'X +M0SQL2B!`]<">Z3@!!$DSRG+CI`!PF=)`QL-^ZBFEQ)=RG@`Q)DH0X3(G*<.( +M.)5#P8&?LG:3$1DI,)&"XP0FGW_5`8#-&"9!VXA(^IQP`$.-0DY'9'J!,.)# +M9RE@$_\`+`G=(B!EVV(2`]6#A$0)W!$2B`R&X.4Y+HB?NG<0.,).@"/K\(&U +M&>2>4.2#D=OE/(`:3N<)SD`@B>W"*0@.D)F@EV-Y1`C3&`)V"3G1,``H$0`Z +M3A!$CL$3C!R`3\I`Y&=NZ!A.@-G!X3&200C=+8$C*;4)WCL.Z!MP)CY3QB9V +M_1,2)WPD\C&-T#.DY.\II@DD`GW[IM7J[)SET[(IR9C`$#;W2`)@3!2&!@"4 +MQ+-,YDH$<>G$1E+`<(XV3-P?=.</VG"B"DZ3.9*8SJVVW3ZAH]TA&G!@JAFE +MT&,<)Q.DG)">)!]\X2C$NR?9`P)TD'(E.R3)T_=,V"#@R>.R=A8706F3L`@0 +M)D1,S&R-XGX[(0<2/L43W"((R$`@F0`)3$SD#`12.1$A,<F.Y4"R<$!-D';" +M-D8!X3^D@X,0J$QIU?[A/480-Y,;2K-&"S2?H.R,4QDD3_=39I4$`Y'PIK:B +MY]1L`G40("E;2R`T&3Q*ZOP'T7^*O:9T@@D?1$MU'5?A3X9;7:RO5H[$1C?W +M7MW0K>E8VP,M$#E<CT1]ITFR8`X-$#<_[[*'J7BHUII6[RYNTB<KGGGIRQQN +M5V[+JO7&!NBFX2L1]:I=.EQ.5D6#JE=P<Z3,86YTZD!$S"\^63T8X:7>FT&L +M:'$;=UH,=C?8J&BV:8#0K5K0+M^%RM=I#TF%S_>%HVC(`'9-;T&Z1)QV5FDT +M8(*RUI9H1N["DR[!4(@;'=.7EA$<HNDAH-<2-N94;[4G\J)E729[*:C6!,G? +M=3VTNE-UH\;3)X6!XCZ#4O!G)RNS\^D<0GI&@1F/E;QRC&4V^=OQ$\`=0K.- +M6@PN(R&_=9/@A_C7PH*E.UI.\EQDTWM)`]P/HOI^M9V%=OJ:T\255O/#O2ZS +M8+&2?8+K++-5C6G@W3[3J'5^HNZAU4E]=[L3.,^_"]E_"+IKZ6EY$`QQ\(AX +M5L:;P6TVD#;"Z#HC&]/;I&D!8RR28<[=I3.FD![1\JKU!H=2,1E9(ZL(`+\# +MW05NJZFX.3ME8RSCMCARSNHO<RYP8@JQ;U=5,&=MU2NW.JOU<3/=#;57-,$1 +M.%YYQ=O19PN]0JT_)P<K!NKD1E6.J5#!))'PLBJ`YQ]1,+6]UF22%6N'.P#A +M`P%SMC!PG93D@QGA3T*328.YY6ML]AMK<N?);GY6I3MP&R6P0$=C0B.<*Z:? +MIR``H2,NI3TDX]U'7I.(P%JFB!E"^BW1($*1JL"Y!IGU3E#;7<'\V%/UL!K? +MCE<_6N7,J:0=UK3#HZ-T7P9PKUK7]_=<Q85G",K8LJI=$HK<\_\`E",Y0&M( +M_,J>LP/TA#YT'3Q\K;*\:H([?L@J5=(D@J$.!V=[[IJSI&#*!5JL`F!["<*( +MOD\9"'U&9W0M!C.,;**D80.<3LG>[U1Q*B<^-MR4SB=."`FTT*070<*-S!). +MQ0PZ<G?A-#B"<_Y*RI8&HQIQ`,\E5;BU8]L1^4*VZ.^5%(D`X6MLZ<[UCII< +MQVEL\K@O&'1:F@N8#MG/RO6ZU,/;!&%C=8LJ3@X.#=E>*SO3YSZK3K6US%0' +MT&22/=;G@WJ#J5W3;,-D3&W"['QMT2TJT7O$!PG^ZXFPMA1OAI$M!WGW6[JQ +MJ7;UBROV.M*;IF1O*E_C6+G+"NT6=($G#0%+_$-[E8W7-\XU]#JN#`YY*J52 +M`?1'NK5?#N!.ZK5A$C$;1"^C]<43R9/?LE$24[],3J@SV0N_)^B@8G.(2;#B +M9@XPDT$X`^R$MAV1D(%$)",SLGW$%+2=])`]T"D:0($^R<QP"D6ZG?ETE*#' +M&-U=!XC)/*<@:>Q]TH@"?V3M&=X^$0TDQ@8"=S1&=B)3R`-`!GNF('Y3OO** +M;WTQ\)&#G!^4OZ)PD-\8'NB:,!)C8)$!I(TIV``;_?*8S.P^RBIKAU%PIMHT +MG4H:`X%^K49WVPH7;[9/9.\&-XG@'=,9+AD&.Y5^AMC),I;X**1)*%D:23,H +M'='/Q,IH&DR-]DPY=)GY3R=$G"!F@-)G;^Z0@D\A.&NXS*?4?+#8&.VZ`,!Q +MB/A(D!I:0`/;=)P,9,2-I3<>R*(1$;'O"8M$1!'RA^3"(S&Y"@&)^B7>(!*( +MQI&3JY28`7?YJH3)B&F`[!2#6"?4F]IPG<UP&D_*!"`"!E$6F-HGW3-;!W'N +MGTSL,'E`@)S#1PD&@$3_`-TQ&0!E$X''Y8WE(':`W/V/=/I))@),$@Y$^YE) +MK2!,!`SFAS9!SM\IPSN)2_,2(A$<.&,`1E1"#<D@1!3-&IOYAOPIJ;):2""1 +M_2HWB'X@HK0LZ8-,8))4[J(+<&",Y3=*!-,8!!5]E.7CL."L4Z1]-L/XBO38 +M#^8P3E>B^'*-'I%@*E0LUZ>#"Y3I56E9GS-/JX&Z;JO4;FYJ>6"X#L"8"7+7 +M3%Q]JWNI]<NKR[\JE4=H!C3.X71^&K-Y8'/G,87/>#^DNJ%M2H)F,%>@=&M6 +ML#1'/'*\^==<9\B_TRVT@'*W+2B2T0(5>RHDN&_T6U:41H!7&NV,%;4,-&RM +MTJ3ALBH49:%9;3TK-=)!4F0-M]Q*GMV0#(F-E'1'J^BMT6SB$;]0BF=\'V*) +MU*?@(W,=P)"E#'Z?=#U5C1&D"#(,PCIVIU=L*=K3,'=3T6:LB%E?52?;.!]. +M%&:-0#?Z+6%$N(X1FVD@IK:,;RZ\2'$0$%6K<M:`#*W76S2(&?91FS!!!:KI +M&)Y]PT9+DS;JL<$;+2KVS0(A5:U.FSME32\*O\35=@G"=MP\'*&KH#C#=^R@ +MJ%[L?LII=M!EZQH.5'6Z@S3+?NLXL,[GZI-I^KY33-J6K7?6W,IF-$>RD`:U +MD",**H^?3W34B;V=PEWIW"NV%`ZQC*AZ?2+W_P":W[&T@`D96:W.#VM$0`!M +MN5:-`%L3(CNK=&UTL!")[`QIDJQ&<^F&S`V]U4NZH:R-NZL]2JM#?W6!U&X] +M4S]%-M:VI=;K:FN`&^ZYBZ=-:/HMOJ+]605AW&*DD+6*7A=Z>X-<"?JMBV?` +M&GNN>LZGKAI"V[)P<-BMZ8K2IO)]1V'"($&(Y5=@=C.ZFI%F))'$*[1,T&1B +M$;((AT(?,:!L-E&*X)SLLVM2;2^ANWU"@J/DF-/R4-2K(,<>Z@:USB3G"SMJ +M8K6(GORFIQJ@C?*9@EN94K&9!Y5BZ$6C$#(0&!,B%9IT^"AJ4A&)`5TS5&L1 +M.^_*A=`,GA37+!JGMW4%3+(!6F=&UC29$*EU1NJD((D^RM0`-Y3,8'-AQ)2, +MY.!\3].N:E-^EARO/+VWN+/J$U&:0X[+WGJ-JQ[(`PO._'/2AJ+PW:2"%TQD +M8]K.&+:5&FV9SCNI-;>Q_P#WE5MZ3FT6MTNP$?EN_P`+E=)IX77R_`C.ZKO` +M$G^ZL5LDD'_55ZL`[8/=>ZUP0C,YPF>#`C9$`-)W33$S*&C#`$2F&X<,)3+I +M!33G=0.7!SB?V3`8W,>Z33F"/[I\3DC*H+29#C!]A,I&.3E-`G3LG/I&2/F4 +MV'#0,A,<SN>Z0<(,PG)#0-L[A`A(,GG9(G&YRG).F!L$C!W,1W1##`P?HF!@ +MD(@(GLFQO)12'Y)'ZIB9;JGZ2G&TSD8^4S]Y;(4")X(@H7&#/ZS*-P)$Z8)4 +M>G)(/UX5-$UQ&0?E(?/*0!!,)X@3!0/Q";U;$[)V_E2'YIF90."9"3S@<I]G +M1N?=`9+23)0*#I,#/[(2""#_`&1;`B29Y*4%W'L@'>$P[&81Z8(S*0;V,^Z* +M`?XA&$33Z?=+CG[;)-V]E`^H:?RC='3+7.EY(&TJ,`!TI^<!#0W#GCW1>=-J +M*,-`F=49*$X$<)G-!@,G43D*H0,8@?5.3D0!A#ICM@_=.TD?1021,>G?&4Q` +M:>X3R8`:9`R,X0PX.#?U[($R7&(E$6[%KI)W'9#!$Y1,^,'L@-KO1V*`3YFH +M\H@)`TPDS$3QN$&MT4C?A;-)M-U,Z78Y'=9/0:>JH&MS\K6KL%%FB#/*Q>S_ +M`!7J57`G\P'>5I^&[9UW>`@.C&^ZS&`UZN@-R>Z[OP/TLTRUY'N"N>66HW(Z +MCH%B*=!GH"Z;IU!K6B&K/Z;1R#$+>Z;1B#W7GW]=,8T.FT1`)"U*%/(@0.RJ +MV+-+1[K3MV"97.NTB:VIG3MA6&4=42E2;#8VE6Z%/GA&I$=&@)_+A6*=&?\` +M)6:3,[05-3I@[?:$:5V40.(4C*$M]O96J5)IYW1BF&Q&Z:6549:B1.%+3HMD +MX.%:;1Q*D9;SM,J6&T#*>WLC\LAL!6Z=N1`1&B-6VRK%40P@$\J-P(_=:3J` +M!.,*)]($0%$VR;BGJ!G=4*]OK.0<+=KT@W,`JI7:UL]NRB,2K;1@&/A15:+` +M!`,K0N2)G[`*G5(CU*;%5U(;DPH*M5K<3A'?W#6-C^ZS!4=7>0V3QA#6UD5" +M]_IX5NTMGU*@)'T3=+L7ZI=RM_I]F`084M;F)=(L,R1]5OV%`#C906-NYKI# +ML'A:5`0`8^4C5B3RVAA[PJ'4,-*OU9T[0LCJU0L80,CNE28L/JM1NDDOCV7/ +M7E4%QRM'K-7)B9*YZL^:D:E(TDNCJ9`S*R+X#(,"%L4V:QE9?5Z6G8+>+GE5 +M6Q)\[?"W^G@B).%A]-9#YX[+:Z</-K!K<*7)J8\-$/.`!,)5''5V]EI6]BUM +M`.]LJ%]JUS\K7.F9K:L:@T1V4+GSN0!W5FK;ALZ9^JJNIN`/IF5S;#4J`"!$ +M=Y4M"LT$`'?NH7V]1QP"HJE&JW+6II6JVHTB-0^BL4-,@@Y7-BI7823("GI= +M4=3YE7E+'347-&T)5C.Y6-9=3IO(!=G=777`&1F?=:QK-B*[;),95-_H!P%9 +MK52XA15QC!E;W$5"[>3[HF;D@_W4-W@]Q\IJ)<>?T4+$U;+28_58W7K(UZ#B +M6SV6N#)@;?LH[MWE#N"NF-TX91Y[4L'-J%N@X*;^!?\`X"NKK,INJEQ:)*'R +MJ7^$+ON./+Y#N3+M,$_)4%PYQ`&HD#8RK-<:B`V/5Q,0JE<-V:\P,;1*]*(Q +MS!]T,^DA%B#F$V6ND.RTJ(`D`$PEQ[]T[AOD?"3V@#!,';"!-/JD&(]T0,NP +MXXV*9\BL[4S3F=/9-`/,R>$$C=()F2?F$PR<X3.;#LS([[A(DY$X[JA-C3D2 +MB&G&)([\H1R"92:`>3[!`3B-6!$Y2:-3PWOW2B=,'/(3$Y@!02,)#2,04&-6 +MV$G1\^R:0!M,@*A'2TDDQ]$P(P=,Q]D0PWN@&#)P(WA%27%4UG%[PV2(,"`H +MQI@[>Z08#OEQY2`(<"Z80+TC&)3#:0!"=H.QC*<M],_E0,-H@2G9I+@1GV2# +M8),[)-@51)QL2@=S<P8Y2]ASV3/,&`9PFG&\F)D(AL\F4FC)WRG<"?;^Z0&8 +M!Q[J*0@F8P>2F<`<@X]RG(.G\WT";8Y$`_JJ$V)SB/=,W_ISRGTD-G'/*0C2 +M21!X'=`Y$_/LDQOJ`G/ORF!!`Q]47],0-D!%HV).>R$M@0#ORI/3_0'("/27 +M&2=D#.$1P.Z>F/5D[);#;ZA"S88WYA`8$F92@2#KR>Z>89@2A`R<">R((MD` +MZA]$3.T'ZE-&T[=@G+73IB>%`;/2\0`83MU5*FV_8(*9),;*WT^BUQD3\(-C +MP\?*8"X>P@*]>.#OZM@=E2IQ3<UL&#M[*Y:4C5J-;!(&\97+*\K/RN>%+(U; +MQKVM)@\C*]1Z%;>70:V`)7(^#[-K7M<6@>\?*[_IE.=.!G]%Y\[MTD:-E1V& +MRV[&F!`P)6?8T@&:C)_NM6RI%Q`C/=<[7;&-&U9Z1Z<+3M:8E5[2EB./=7[6 +ME#?=9=(L4:8A6Z+(XPHJ#/3D25=MV>K))114VQCG?"G93<8Y*EMZ4%6Z5(1@ +M1"U$VAHTR"9`^ZG;1U#:)1LI@'T[JU09W(CE-%J"C;Q(`R%89;D"8W4LM#AG +M=$XC3(X2QG=1>6!QL@J,Y4T^O'U0N!T$`[J"/2(@M4-9K8]U8,=Y"AK"9VGA +M04KPAHP2LN]+0"=EIW8AIET3W6)U.X:`YH(6+6I%.ZJ9QRLZ\KX(`SV1UJI< +MXL;RHA:U'ND@=UF+I2<U]R\``K7Z1TF(&G=3V%BSS`0`#.P72]&L2Q@Y(.ZO +M:ZTIV?3M+1+-EH4;4-`&GZK6I6T>F`$YMI;/*UI95*@UC3^ZL$`-D;%)M&'3 +MI@)G@AN\J-\`KO.GNL;JKO3(XY6K6.!WX67U$@L,]E$TX_KKX><PL&?,K&"M +MOQ/N[]I6!:`FN-HE).&,FO;L)I2?HLSK8:&;[+<M@#1(*Q^N-),0(A:CGW6= +MTN/-#"=UO]/HAE36TB%SEJ_RZX.-\9716Y-6E((QF0L6.DK?I7-)M/\`-^J= +M]U;L'])*YRNZL/3/J'NM#I%C7J-!J$GZJ7*QOTFMIZ]\UQ<0V(Y47\4V-7EG +MXA:]GT=I:"6@K1H](H`QH`^G^BZ3#*N.7DQCE*ES5+891)G?"@UW;G8HNCL0 +MNV-A:4A+F-'T4+J5H)/H('"Z_MW\N?[T_#BKA[J=,^=2(/PL2[O*9K:0`%W_ +M`%*QI7-,@`01D+E.K>'',<:E*,"<J7"SIO#R2]LO348T5&.Q\JWTGJTU?+J. +M_58G4*]>WFDX.^(5>T=J.O7Z@N=GX=-_EWS:K7LF9!Y4;JD3)E8/1NH#%)YY +MV6M4<'&6D0=H3_#1KIH(G49]D#):V=7NB![C/LG#=+=$9*U"](VN!=`&?<IZ +MS`6.U9[05$?^87<A/5J_RR=EO'MQRBF^BPN,0F\AG8)JCZ9>200?E-KI^_W7 +M5P?'U8`%QU2,S_95JA&GXXY&5/<..HD<?15ZCGEQ$-''U7L<T;P-@`T("(., +M%$\C)D8.0FQZ2@1EL@[C$H,<E$8F0A=!$D\=T#@M=JG4#P1_=)HDSC=,-.DD +MB'#MLG!`!`_50%'J@A%#2_.!'W0L(B)`.\RD"6G`G*H0:/I/V3AO)'V2:Z-C +M[83`M$X[YE$.UHTQG.V41$#O[E,-(R.4[C/]6/9%-@N_=+2T$['^Z:1J.G9* +M>W"`B`6D3/\`=-I$3N-MR4X@M$P=/!35B`XM!+AR=N$-A(AN$VF1@?=%4+=) +M$1]4FX!P2@30=MCV2<W>?E(F#&\8RFG!SG9%(F!`^"D!Q/T2!`/,I2-<@$0B +M'<-1!!@3"%['DP!MB`B);]1RB)D2XR9Y0`<-DREI$0=D\MUD@[;2B9),B".0 +MH!P03,<IBT:3!F43C,Y^R$F`?5^7"JA:-P1O[[(],4QZ@9[(0?41!*1DG),2 +M40\`'&!RD=42#[B4[)+,$9,I-,GU'`V^44Y._J1:AL"A,C(/.Z6I-(8B792- +M-]/)!`(D2,(\AN")&4,N,R0.X"!VZM\PGI8<)_[I3`&W<RFV``.1.2@<F9`R +M.Z,NG<GZ(6-)J`3MRE`:)!$<*"2W8Y[A^JVNG6;VMD_U+/Z6QSZLC!G,+K7V +MWEVS06Y/`4J6LVWH5:K@P3''8+4Z?;%K@UNHYS!Q^R'IM-H9#X).<\+7Z!0# +MZ_J$CD0N.5KIBZ?P?1:UNIQ@]IGNNSZ=1)<#`6+T.V93I"0T<X73=/IYWCX7 +MF[NVXT;*D-#<$!:_3Z.F"(@*GT^CZ=EKVC`,K-=8O6E/V5VA2`SRHK-HR`K] +M!HD#*-Q)0IC8A7:%.(TA0V[,XV5ZWF`8E%2V[,#"M4VDC`P@I`00,0IZ$C*T +MR36$>EW_`'4],#5'9+^K?/.$3()$;'E0-4;.3VW2`,`$R1V1.`'NG8W2Z=_? +MNH!T`YXV2%/<SE2N`D#NI&,!&?N@JFD0"0)5>[+:8DK0KD-,3L.5S_7;P,81 +M.2LVZ63;/ZU>L@C4N>J^96JG<B5<<RI<UI()^JUNF=-$:G-$^ZY[;Z9%GTUQ +M!<]ICC"MNMVTF_E@]H6\;=K&Q_9974=+'&9V5TDNT?3F#7LNHZ32:&C&`N;Z +M29JY75=.+6@"-L*XM5H4:(<,`&$-2EI)@84]"JI:;0\C&ZZ\5GIFNI2/<*K= +M-TCU2NA;:MVC!5.\M`9V'NI<5F3F[QL#&RQNI$$.]ALNAOZ8;J@RN?ZH``3V +MS\KG>'1QGB9S1JS]UB]-@W!+5K>+B"TG;V6-T36^Z[:3]E/C&3IZ+8MS!SLL +M?K+<Z=^5NTJ?\@>_=95_3)?EOR4E<Y&#<T?+$B)&0M+HEZ�\S*:O;:J<PJ +M#0659_1:LVL_#H@UCJQ>#/;*Z+H=:EITN@G;"XVRN@)#S@>ZU;*Z-.'-/U6= +M:JW>M.\M7-;3D\*AU_KUITZF2^HT1W(6-=>(&6]@XN<!]5XO^)GB^O6JU&-J +MNP-@3C]5Z<.MO'GWIV_BS\2Z%,O;3K8'O_JN0J?BJ[SQ%<P8V/\`JO#?$/7K +MVXNG`5*AD_E!.5GW1ZE9U6/NJ%:FTY&MI`(7;]JWA)I]3>&OQ#IW,!]5IG$$ +M[+N>F7]#J%MJ:09XE?'OA?KE=I:"^#N).R]P_"#Q,ZJ!1K.]0.<_*XV7"\EU +M\=OXIZ2RJ'56B"1A>=]6\WI]SZB=(.0O6[A[:M"<&>%P_C?I+:]%[@T`1O\` +M93/'<X=?'Y/E8]K=,JL948\2.Q72])O?.HQJ]2\ZZ9-G?FWJ$@'8<+H^CW6B +M]:T.])[_``N%M^O3K5U'74G;F!"EID'(.>P4-L-5,3!'96*3(GNMPM0W!#7P +M<SNH:M5NF0,*>X:&DR>%0N7RSM"Z1QR+1J]0>(*7E'_&%6\QO^RG\QO;]5UT +MX;?(=S`,`AP[]U7J"'=YR)4]P6DQ(D<GE5ZI;`(,[X[+UUA'/&X&X49``!&Q +M1G3)GE"8@;'X40TDP#E,,.G8A&7$L#3$`R$((&44H$D&)PD\`G?(38U2)SLC +M/Y8QG]40P(R!]^Z<:0Z7'&^R83SMPD021P@.D01$[\I0)V&W?=,W;$@_*;82 +M52G#6C(^436MW<^,;^Z$9.R7^X0)V7D[Y3N;!.KC:$@V$S]I/^PD#Z6D1D$( +M2UHF<E(CF9GA(-)('/RH&`U'YQW1`$?/=`"0>Y[(].F6ELGB#_DJ:"0!L#'M +MRD(_*1F-TGD%VXPD"-,R"XJ*0'H@[CE*'3,_ZI/`(Y^J=FDND?9`@&:#).K@ +M<)23C4E5!D'(D2A8`AH36G8'/L81`[`B.Q/"%H(_F3@8[IH9)R8^TH$3`B"G +MR6:9'>4_Y6G;`Y*%@D9)]D0S6#`GZH]()R=DP!`&84M33H])@#@JJC$`=@,` +M),P[5!(V*1,Y,`%,W<F=^$#P[D(@TZ2-O[I1$"?HF.#Z3"`FMDY(G"9S"XR" +M??"<#`DP1[ILCU<?V1"`]A[>Z<`.=$B?V2`#L1_V3DD`8CA3H,`<F28V[!.W +M43E*"=L2IK5H-5K-LS*#J/`71ZMY5%31+>_?V6_XP8VW:VFT-U;=H5[\.6&C +MTXAK,D3\+/\`&.IUZ6NF)V"XYYS?JUAX[;[52Z)0\RL=8+I@8*[+HEGH:V`- +MYPLCPO:%T$@D[X7:],M0W3I&%P\EVZ3AI]%MA`U<KI;"B&M@9[+,Z50AHU#E +M;UE3U$2V,+FU(O6#7:1@`K6LJ4C\H5&SIF=_JM>UI'3[^RC<6;9@:9$*];4S +MA0T&`$*_;T8Y*-;36],`CLK5!D-'LHZ;6@*>UAPGCL5=":F`0`3/]E8IC$<0 +MHF`[*:EL!,!6"4-]0,HP!'RHV[_W4U(2)[J$`W>8(A3,9)S`A%2IAQRIPT!N +M4T(6TQ,$\)5H8P03E2NTB?=4>HW+64YD*45>IW(IL))7+WCC<5=,D\JQU>\= +M4):UQ*/I5`$!SQ)]UQRNZZ2:2],LVL`)9GW6DXTZ+,B!W0.<VF))B,K!Z]U- +MP!:TF"/LIO2^NQ^)?$-O947_`,QH`]UPM;QK1N+WR65&DSM/^JI>+A<7K7_S +M#GW*\TNJ%?I/7J=1Y(:X[Y@J7VK>&..^7T;X:K"XI"I,SW74VE2(_P!RO//P +MVOA6L6YS_P!UW-O5D`XE7&\)9RV*50"`#A7;6L0T$D2L:C4(]2M4:OW"Z3)F +MQLMN1IW56]N?285&M<:6D3^JI75X,@._T6KFDQ!U&JV""`9"Y[JC_28C*T;J +MX$.AP^%E=1?JIGV7.UN<.,\6.!!D[K&Z!J%R6G'Z+>\3T=3'/VC]5B=#9%WM +M,G"EZ+'7VXF@`<E5+F@#QMA:EC2/E@XRD:0-0J1AB/H$B%E7=!P>2W8KJJ]$ +M1$+.O+8%LP`MQ&`ZB1D'/[([>X=3<`XF#[J]5MP&$9SLJ%S1+7'>.86M;5<N +MZ(O+:`001C=>4?BAX9NF:JUNS#1D0<[>R]/Z9=.;4#7-AOLMFKTVTZE;::C& +MND=EK#+5<<\?KY)\,7%ET_QI:W'5Z`?;TJFI[*@X'RO6/_$'XT\$7_X?OL>G +MT[*YO:VGR'TF-FB`X$Y&TA:?XE?A9:W=%]6VH-94`P6#_1>&>(?!W5.FU1Y[ +M/29&QD1$S]UZ\?[>^WGRQF4U^&9T6L0]I<W\OZKTW\)^HO=UIM,-($P8^JX3 +MIUAY3,,)=M'NO5/P=\-5*50755K@7&<K/FU88O9[2HYUJV=U0ZN/,HN:3L%< +M8X4J$3LLN]N&ZCI=@<+E>(N$W>'`>+Z1H5!5:(`.3"BZ7?,?7I"#+MCP5T7B +M.C2K6K]0W"Y#H]L*74H!($X,KAY/R]F&OKU+I%8.MV@X,+0:26R<K'Z!'ELW +MD>ZW'M`9(/RKBEJG>G+O5CGW61>U0`8=\+2OGQ.?E<]UNNUC'&?U7?&..5X5 +M*MZX5"&D0A_CG_X@N?KW;_-=IV0?Q=3_`&%W<O6O`[D#S"7&>%5JN].D1!5N +MX+7OEK(G8`[JM<L+':7"(X7>N2%S2,@$2)R@&>%(X3$ND(1`/?V32ASI(@I@ +M.-T<NR!]<I.+2[`COE`!,[82>T"(TDD?;V1L$B"\#G)_1!IYS]U`B#JV^R0D +M081-`#ADHB!L'?W5@8DNW'Z0$VDGZ(X])WA)S=D#`^F,S^Z8"<1E.W:"=L)$ +M$"90(3$29W14C#FRT'V.Q33@YB.1RF(@9A`3RW=I4=:=3B'&"BS`@@2F<YP& +MG60#N$$;OM*<3L2?H4G:NZ0S$_LBF#=YF$\3.EWNDWDDGX3Q!,$*:#$G;4G: +M3.)38)C=(G$*AR3P283M)/=,"WE.2)B`1WV4#ZCR3&TIL[`PGYV$!,_?@]I0 +M(EP&28"%KB8!/W18#>-LYW09)[J@I))@Y"D-1SZ(IP(;[*-C9,$@>Z)H])R$ +M#$&3V[A(`B#"D;ALD84CRP6X:6#6#.J=QV326H7$XS/PF(=));]`C:9(,#/= +M'#8@E#8!J()&0/9"3#I<V>Z.!IF"`>$PQ^5L'N@.D3.!N-TU0D-@3*3=X@9V +ME$]D@X'R%`!(#F@".,KHO`/0J_5NIL;HEDY*PK.AYM9M,'\R]J_">SM[#IS" +MYK=3LRN7FS]8Z>/';J.B]!I6/3?+#`-(R>ZX+QC18[K@I,=MV*]*ZSU.C0Z: +M\AW&%Y:*K[[K3JO+BO)CSD[R:EM;_ANWAK1I=\KM.C420"6K!Z#1`<UIQ`C; +M===TFC#`,I]<JO\`3Z1:X`C=;-K3../=5+*EM#25K6K02TD05*W%NPIY$A:= +MMC(W5*WVQPM&U:3[?*C2Y;#8PKU!P)WA5[=@(^%88T#8*JLTCZ@)QRK-NX0? +M3"@I`R.59IB1/*"1F'3F%,P^J)^JA;JGCY4]$9R,%3:I&@D[%3T],?""@UQC +M;Z*5[6CA!(VII:9":I7QARJU*P!W52O<-;JSO[J6FEB]N]$S@+G.L]1)!:#O +MV1=9OP`6@Y*Q&%U:M!,K%R=,<4ULQ]:KJ,Z>RW["GHI1!/NJ_1[<""0"%M4Z +M;`T8CX7.1M0N*3GB<_"PNOT&LIE[H$!=:]K0"3PN3\8%S@\,)5LU-D_#A^J> +MNJ?9<=XXZ36NJ(-,B09&%W#Z+G5O4/JAO.GBK3RS"LO&EN.KM%^$[:M&T8RH +M22`O3K0N\L?YKA_"EJ;>H.R[6U>&@">-RL8S2Y]M"DX3$J1U9K6Y.0L\W`8" +M2<!9/6^N4K5A+G#'NM[9TV+R^`)`<LVO>R<N"\W\2_B):VE8M=6`(]UF67XA +MT;FM#:HRL7+ZZ8X5ZHZXU[F5%7<U\@1\K`\/=8IWK`=>>RZ&VIE\'/\`FI,C +M+'3'ZW2!ID1PN>Z93#;T;[KM>K6Q-N[&ZYFE;Q?2-YRM6\,1NV3]-,@[*>FT +MG)RGZ=;%U,$;J\R@`?WE,>6:SZM,021"KOH!_P#3]5JW%`$8(*A%&&G"VS6% +M<VL&#PJ=U;`4W-Q+AN5T%Q0;JG]U4JT&G<#/*VPY*ZHFD9`(*N=$Z@^F0UQV +MV"N]4M@>!LL>O0<QVMLA6KW.74FM3KVY!`]EQWC'PQ;7K7.\D$GL-EK=*NM1 +MTDG_`#6D]OF-@D&<JXYZXKEEAJO'*/@ZA9]2#ZM,%L\KO.A.M+2@&L`"TNL] +M-;5S`^BQ;JS-*F&M`$<K6^>$]99RT;[J%)S(:1]UF0ZM4]!D$K+N65&F"=D5 +ME>BW?+B2`N>6=^MX82+?B"U>RS+78D+DK*FZGU(:OZ3L%U=[U)MVP,F)[K.? +M9M%<505G/5G#4NNW0]`+6M!)(A;AJ,-'4:D'@+G>GO#6#U+294+F$$C(X*WB +M9(>HU@1#>5QWC"[\J@:<P7X"Z7JA&@DN/U7"^)W.N.JT;=N9*]&$^O/E?BST +MOICZEA2J%IEPG]58_P"$N_P%=?T3I8'2J`.^E6O^&-3ES]WQS5W]APJIW$`R +M5:O=+JA<&``F855X,D@8]BO6RC>3J,[\@I2P$Z09XGLDX3).23W0/W'<;H$( +MP3E,8G?!1`9Q"'?!_P"R!O8@GW2';!1%C@W+26S`(3.:0^1('$JAA^6(B$6\ +MG?;"%HR`"B(@Y"@+4<'@)#\LF4#<MW1-((&-^Y5#YVXY3/'<$)H)"9X).24! +M.<!L9^B0.?9!F$T3$H)`6')&3NA?#N8'NF)SN,)MW3!'>$4HVQCW"<'./W3` +M83!L'`4!.;!("8>[3LEI`,28`S*<`:?9`P@`04@)G"1$B-B$6F&_"H;TPGF! +MQ]TS0=<3&.4S=Y/'NH@CEHS@)`#5ONG`(W`^)3&)[>R*?T@'T@CY03Q)E&V( +M(WG;A,V`_@9V"(:28R8E'ID0-TY)#2UKX!W2!@QW0)H.DR["+4(@Y*#$P#NG +M`;OPJ&D`@?NB<2`1&>$((!QD)<G5'V0&R'-C?Z)G-C))]A":D&EI#0G@C&([ +MR@>F)=)!PC,MQ*"F,F!"EIM+GC2TDSG*@N]%8'7;7`1G.%ZAX4N7&M3H:H!@ +M&%S'@SI%-U'4X&>Z[?PCT84KP5BTX&Y7F\MEX=\,O6+_`(W:VAT@PXDQB%R7 +MA.@'W7FR22>5L_B9=Z6-H!RK^!+?8N$Y[+CCQ+8U;P[+H=``-DB>5U73*>1C +M'98O1Z0$8PNCZ53AX)V4VQ(T[)D1Q[+1H`$P!E5[1H!SSP5>H-;M*S6XMVS9 +M`,%:5N,#"J6K1I_-^JT+1L-U;A%6J$Z5:8`(]U7H@=HE6!M`$JJGIQ,*=CL1 +M.%!1VR-E8HB8,9*C6DM$?4*S1:V1.%#0:6^TJ1U4-W5V:25:[J;]-.GC<F=E +M%=5XY_55[BY`$ZMUGW-QO!E9M)%FYNL8&8PLZYN"9R@=4)D:OHHG-+C]%BMR +M,[J=1SGXF$NFD`^KNK56VUSB%&VV<P86+MVFKPV>G5FM``6BVKK:`#"P[*F\ +M/`+EJVI(@?ND6Q/4<\LCA974+!UP3JV6TQNJ!PIQ;-TSI]UO6V.G%U.BM827 +M#G=05K#2=L=UV%[0:&N`;E9-S1;L`5-2+.6/96K6OD"(6FU[64LIVTAO^JAN +MA#2&\+#2IU6ZTT708,+@?%+[BZ<YH<0%V=_2?5:1)@K'N[`YU-3MJ33PSQST +M2]JW#G4VN<)69;=+OK=C7.:6EO*]TN>F6[YU,&5D]1Z-0J/T:0>(6KG9#'*N +M:_#?JU>C=,HUB8G&/]%[CX=N&W%HTC)/N5Y=;^'Q1K-J,:`9X7HW@FF:=%K2 +MXDQNN/,K6=F4;74&`T"#&0N9-$-ZA`&Y767PBD8,K`#`;[5L96ZY1M]-MR+< +M1V4WDN`("L=,9_(:8X4U1OK.%TG3G6<ZE`@A0U:9#96C5I3PHJM*`<$K49K* +MK-!/^2J7%/>/LM>K1&8'TE4[FF0#`!51AWM)L:EDWM$`D@[KH[JB(]0"RKRB +M-41^BC6G.D/IU"YI+2M3I=X'0UYSLH[VD`2"/LJ<>55#FR"J6;=`ZFVI3G"S +M.J6@+.ZGZ=<RT!SOD*2X<UV(P597"RQR?4K8"8"P[VB0X@$Y79]1H,<3PL*] +MMOYI`B%N\PG#&HL<*@)V[E:#'M%/)!)&Y45S2%/U1D*M4K@-U2,=UPUJNO<; +M%BX&)S/NM`NTLV6'T5_FD$#$[K8K5&LI29F%UPY8RX9G7*Y92<22,+B>AU#> +M^,=.N6M.P71>+K@MMGD/CTG=9OX2VC+KK!K%N=>Y7HG3SW\O7>EVX'3Z0#?Z +M>58\@?X5IV=LP6K`!B%)_#M[+K'E?`-Q)JDSG=5WP<RW'ZJS>.EY@0"<<JK5 +M?G!)^5VKLB<)&,DH,[`C*E<1.VWO*!Q;I(@ZIP>(4-!R3,),'I(D=Y3EPC`G +M*$F3/]D#N)VC?A.2YP&HR>QW0!_^RG)Q&^-^Z:#9!B-NX1GU.)(P3F,)@8G; +M/UA-)D-)$#8*AY$!K6DG^R4XVV288[#Y3N)!(TX]U`S2=XA-F8C'=&S:8QLA +M<[.V1A`T@-T@9F9X2).F83NRT2`8"%Q((B$T'`)]1!SRF<T@Z<&1/=2LK.;E +MI=MI^G9`]V9C`[*J$@Q^6?A-)&8*)N4S@-YE`HR"3]-D],P8C8II'=(QI'Z$ +M*!R2-FE,3&THI`&\D'9`"),H$>#.R<9]Y2!W`&4[2([_``J"#HD&"(V*8$;C +MZX3$B,3GW3LB,$SQA1"8`23JX3ZMH/RG%0:1Z8@S,)C^>50\C5P)2)$$P/9, +M=$A$S000XG:0BADX[)-/9.V"1.>"B,:1IC"`&"&_*?&Y@?"=X$0(CDII!,DX +M&\)H)OOE$.^`WMLD0-YS&1[)@?5B/[J(=ADB`M#H],5+EK($SLLX1W``XE:O +MAMS/^(4]1`$J9<19-UZ1X.L7-HTSGX'*[BT'D6VJ,`25B^#:;/X.F&Z5T=YI +M9T^I$8;_`&7S[;M[,L9T\W\;W)N.M>6TB-41]5T?A"EY=LR2)A<E??S?$3R9 +MPZ%W/0*(9;TY'J,!:O&+'DXNG5]%&V%T?3&-P,@\K#Z'2'E`RNCL6P!@CX48 +MC1M)#8._RK]!N0"JELW`D-^5>MFR!$K-:BW:#(,+0MP!OM/=4K89VV5RD1GD +ME%7*)C`&RFI27*O;\*S1&>))V2M1:I``S*LT0,$E04FY$J9KH&!NHJ8OTM]X +M5*]N!,:LA/=U@![K)O*^IQB=E+5B2M<:G$`D*(ND$3E5M0D[H@2,0IMK20X^ +MB-CA`(.5&R'$#E6[>A)]E%!2]3NZLTK<$"1NI:%L"9A7;>B(`(5TOLJTK6(+ +M6Y5RWMR`)^RMV]NT1'"L,I,V5F)[(*5,1B,=U.UOHB%,RB`,DPK%.@(!W6IB +MSMD75OJ!.!&2LJ]MW-<3NNJJ46EN6CX5.YMFO#F@#/)4RP=,<G*Z71@;*-]) +MSC,86[5Z<W48F"D;"&@=NZY>KI[1S_\`!S,A4>IV8T'V755+8`0,E9G5*/H( +MVQ*FE<)U!OE.?MA95-CJESM,E=9>=,\^L0)AVZL]/\.`.U8.%B[IN1B6=H*F +MEI$+J>B6XI4P1`14NF"B[;;E:%*D`P8XX*2<I:J]3>!1,[A8]@T5;V?=:/5W +MM#2TF.%#X>M]5?61REYJ=1TMC1B@&B9C=$]FY.ZFM9%,<!'5`[_5=XXJ%1I4 +M>B2<A6:S=1C:%`6_>%4JI6G@?*HUQ.P^5I5VAQA4Z[(;I)"J1GW=.1,02LVY +MHF3^F%MU&C3@@_*IW3&AIG[J::CF^H4C!D[%8US#:G(@[A=-?T0YC@!(6+>V +MA#C@&5-MQ3MW.#Y!P<K0;5)9E9_ENIO@"0,Y4[7X@'C95SRA7A]&#*R[]OIG +MGV5ZX(#9CA4*YD<@+<<K&7?,+V1)G^RY[J-9].OY;1.5TMZV-7[KF^K4GNN` +M1,$\8A9RGTQO+H_#%*+=I(@]U9ZM4`)@^RJ]!?Y=D&@Y`0]2\VK3JN8,,;)R +MKXYPF=<GXVO/Y6AID@[RND_!GIU6F&W#@<G405POB=YKW[:8[QDG)7J7X14Z +MK;*DUV,`8^B[R<R.&?$>CV];31:)B`C\_P!U7&!&HI9_Q%>OT>/V?!5TT3J( +M#>T&56J@1+3DXRK-P/YG`/;=05&>K2X<*O2K$Z9`!!^4GMTZ02<Y([(W-&WN +MA,2>W!)4`;B,;IB,Y*>!ID83Z,$E`+A.!*9A)&Y`]D<YF8^J8@S,G[*A-;NX +ML($Q,)#\T'[!(`<S,I1+H`_T0(&=X!"3L#<X/T*)H&C;,[IC+G:B20@9KO3C +M.$B&G.T\=DA`W"(@;9A`.YSA,[#2/H%(X2?D90M:)(C$_9`+#C]4=Q1J4M(J +M-`-1H>,S@[)F$MJ-J`!VGAPWA27ES7N7,\^IJ%,0T1L-X_5!`P-`,C!_5.(X +M&R>!`<4Y:-.HC?*`($[3E-$$1RB($8R)[*ST9]K1ZE;U[VV-S;,>#4HM=H+V +MSD3PH*PS.,A,YQU2[C`^%/<FD^ZJOH4C3IN<2QDSH$X$\X43J<F.(^R``T$8 +M(F=DP',<J0T26X!_R3FF2XC(X$E4"TPXB`0<2DW$1D1OS",L(=Q[%,6$^HN` +M^2H&$1./ND^`8&>^488=,EPC@S"8@#5(:<1GA`+FM!'O[I-TD=C\I.:#_4`# +MV1A@+8)&_=4"($<HZ+A3J:NV1'?A,QD"-6Z=S&B(<!B4`AH.2[)RFD2?43]$ +M>EHD:D_E,`:2_A-@&'W/=%&29.W?=/Y;/\8']T!#)F3"!L<D[[J:W>:-9KQ\ +MSV0!LMEDCZH(PX3LH=/6OPTZ[2T4Z%=X#L`2?9=IUR_I4^CU"QP&L0#*^?>G +MWE:U>'TW?E[+;/BB[KT!2=4,;+RY^&WIZL/++_9TW2GBKU\O)D:H_5>B]*IB +M&,#3``@RO-/`G\VY%0Y)S\Y"]0Z,"6,Q&Q7/.:X8SR]KMU'0V::;<[?JNBL6 +MRT`F,<K$Z.STMG.%OV`P#N3NL$7;8`&(5^V&`Z?H%4H,]0,>Q]U?MP<3\J-1 +M98(`D0K-%IB95<$@YV'Z*:@\GX^4VU%VB,`JU1C"K6WJ`5IK0`,J5J+`VU9" +M:I4`:/5!0EVD8^RS[^X+=7J@!2J'J-Q!/JQLLXU2Z<SF=T%U<&H^)4=!QUP= +ME!8:TDP"9^59H4G:<R5'0;)!!]U=I#/;W[*:78K2E+CQ')6A;TAJ$`3ONHK> +MGD3E7Z%)P:.<YA61+4E&D&R>RL460)A-2!=@8PK%(-@3\0M*DIB`TR,J2F`7 +M)`!V&P2G9^;M"HL4Q(]E-3$-Q^JBIG/]E/3$C/PMQ*"KANP,JNX'=6ZC-\J( +MLA^)"595>G3#B<"$]6D"V8E3,`#\B/=/6(`(^JFFMLRM1R8$86;U"WD'`6S6 +M[XB%1O=);@;KG9'25S;J.BX@C8K4L-#F`1E5NHTAKU-$'=*SJ^7$F5QZKI9N +M+US2U<1"JW3A3IND20,*=]VSRSZA*PNMW^#DB>)2UF1G]6K&I<Z`5K^'*9#! +M(^JP++57N)W!.ZZKI+0RFULS&%,>TRZ:U'#!S")X!&-^R"BZ![E2O`^(79R0 +M5&&9(QW4%1F8C'=6G-QOME05)F`JE5JK`!@&53>S)5^O.D\*K4VF=E44JS-! +M)5"\!+<8E:=U!DA4;B#@G)4VU&749)(GC*S[VE`+8,E;%8-!D$*A=AIU1OOE +M-*YZ[IN;4)B.,*`@;[$\K3O!/ITX69=L]4@^\*QJS:.JXND$<*M5T@1"FDD^ +M^RCN"-)!&58Y933*ZBZ).^E85R]KJL-'J&5O=3PUTD?*PJ[8?M))PY,JXKMC +M6+:6(B),*OU*\++=[FGU.!!,*%U;2T,&_99_7*[OX9P$1&4PIE&+;UA5Z\V? +M5Z^WNO:_P_I>784_3!(F>Z\2\)M?<]>:3_34X^B]Y\*L?3Z6QH&-*].$_D\W +MFO#3?<L#R'-,C!A-_%4_\)5.L[^:['*'5[+W/%M\35H#B2#CE5JH!XF%8K1) +M@P%`^`X:_4#N`N;VHG$Z"T_E&0/=`?B92?ZC[#8)H..(,[H&V!E)H`/_`'3O +M:00!VF4P&0$");DP!/Z)B"6@QCO*)^<F7$<RA<XP`0"`H&W.HDRG!`9G&(W3 +MD@DN``D[#9(P)VGYA4)H]4B8Y$H@.QVW3-,[`RB$'!*!M(T@YG:2FG$2?E$T +M8[I%IT$D2$#;@0XR1E(8&'9.$FP.9]TG;",>T)0YTD[X]D`&X+D0@-$B?HA. +M1(R@=C?<P<I">2-LE-Q,0@:=.YQ\(">WW3`Z<R92CTS'SA(9.1L@+4Z!)3!Q +MU'8)W`$`C*1#7-)_+`^Z@$O=J,.,)M9<6R2F,S(28UKJ@:7:9_J*JB#G`G(P +MF!=@G<I#!P9,[IB1VE`@2#P.,)VN,&2,8RE(G_-,T2=(]1C;=0*?3G;X1AQ. +M&D1*!PSP([E*??Z*B1I,C&R<ENG!!GW0-!:S43B8W3P)B1!4!`D`GO\`5,\G +M5G)XDI2.\`>Z1D@095(4F/68E(NA^_QE+20(SG8]D[A#@`\N`R3L@=CM.'-D +M1M*$.,'3NF:`1C]24I]1C"B#IG`](GY4M!H\P0/U45,8!G92VT>:-P)RBQZ) +M^'E.&LU8C$_9>H]$;AITXP%YK^'%+66DN:.TGE>G]$IP]LU&G"\7EFZZ1U71 +M_P#EMF8XA;MF8`GY6)TMHAI6W:-,`X^JYUN-*UG`S&ZOT(`ET_'94K1I(GZ* +MVP:>2HTG)D8)D[*2T$B!^ZAI9V<KUHP'.!\HU%RS8("L#;V_=16\-`SPC+PT +M079[J:VJ*\J:)D\+&ZA<:I;B>5<ZE6'JTG*Q[DDN)/*R!8XET'\OPK=O3U.S +M\JI2@>TJ]9MDC8B4T;7+1AU?FPKE$MF9D[95>@P`[1"O6H$[X*?\6+-J#J#B +MM*W`(`GW5&DX-P#*O4*@:!ZLA6"Q2;Z"1]DS"2^",;RDVLTX$2I&:"Z9&RTL +M'2,84[#J(,`2HZ6DN$?=3!XF`=D@D8R/ZL`*1CHV.V84#*ITZ<;J1KLC:2M" +M<.PF$%V^/=1.?I^4[*D"<*[-)7QI)D;J*KZA&)3O?)F,)M33)4%2L#JSG*HW +M^)P%H5R-EGWC06F2L9-XL;J-32#E8ES=Z'D-,0M?K`Q`.>"N=NZ9+BX$X7#) +MZ<$M:^/DDZ\]ED7]TZK5#9QLFO2]F!RJUI2<ZM,<_=0L=%T"D"QN`)72=/;I +M8,+$Z*P-IMANVZW[0[?"UBXY+=`$NF,*R6:LGLH+8^KG*M2"(E=,7.H'M(DA +M0/$YB%9K8!C*K$R2(SRM(AJ06D'M]E3K`D?*N/@XE05&C/8<H,^X(F#B%3N6 +MMTDM_17;QH@F=\`E4:\G!/'=*U%.LT#X&%2N\B>>`KU;_P"["S[P2^`?JC3. +MKB7$[$9^JH7%,:'<DK2N6@`DJC<B!$ITK+>0UY]E!5?+C[*6[:0XF8$K/JO` +M>?4<JLW$-Z&$96)U!HF)^/=:]T\%N3@+&ZD[USJQ]T^//E-51KO`>0[A8OB> +MX:*+@2&X[9`6G6,N=J<X97,^*;@.+QJ)SF5OQSEFM/\`"JFRKUD.=!@_W"]X +MLGLH=+9IWT_V7A?X1@#J`<7""<#[+V,UGML(.T?9>GPS^3Q>>HJUZSS734C* +M'^-I_P#R+!N6N-=YU')[H-#O\7ZKW/*^7:WY@V`3['=5J[8)])'"M7`!)*KU +M-);.?A<'N5WD;<E#I&Y'V1G)W.$Q(TX)^J@"0';?0I$X+I(G$)P`1F04SVN` +MQA`&HDXF$^II$"?JB(`=.1^J8P3OCW0(E@,M)]TL:I((!2(!Q'WQ*38`,EWT +M*0.TMP&X]T[B`,Q.R=L;[RF@3B539PZ/C9(N]\;0FV.GNDYHWV0+$2#OA*I& +MF2A`$P#$(J[FOJ:F-#1VW40AI#07&(]DB0YL`3R7<I?U>K./A,YIX./A#H+0 +M"(V^4B2T07$CLG@1G<;82<UIYD(I'8P=T(@1+D\#/JE,6R>ZH(")@X3.+=)W +M)3_T[(7-,?FQLH%)$PF$\$%/I($2DSG2=MT#`NDG5D^Z6VQ`QLD0>=NR$P!W +M^J51$_U2,\2GI5#3,M,&(D'9`UH.Z<@!`MR8.V8)2/M@]DT`'O\`!2`$D;_* +M!QN<A.`(()CX30=P0B:#J.HH'+=X.R;8C$_">,Q&0DYL8@;;('#LG`@IHSF` +M/A.1C:#O@H3ODX0$T8P`4B)$:1/>4FMS,.A)S9=@[(@F2/\`)3V7_/;C"@`X +M*L].$5`,CX18]'\`:139(X7IW0A(;"\R\`L,,#B97J?06AM-N%X?)/Y.N^'5 +M=+@-:3QNMRR`+1DQ&%B=*@M&(6Y:B",;K%:C3MP/+YV5BGAH[2H+0"/[J<8? +M"RTEI.@C'*T+,`JE2:(SLKE"&MW1J+3<`DE5KNL`(!QV2K5@&F52N*FL0TY4 +M$-R^2294%>JPT@UM.'3.N=PFNW$-,G94PYSZ@RB=K5`-DP9/*O6T-V!PJ-!N +MD@;*4U-.2Y1J1KTZK0T$NA2_Q8#0)(GNN;K]2T#33,RL^ZO+UP])(:<[)MOU +MUV[=G463ET0IJ?4Z9P'97FU"^NGW'EZX.RZ+IM"Y>`XN*;OX6W&?77TK]D@Z +ML0K=&^8X8=E<U_!5A3U:R.52_B;BC7(!)"F5N/:XW'+IZ!97$M!+E<I%AR25 +MP=GUBJP`..GY6]TGJK7#-3]5<<I6K@Z$C`@!."X9;((4%O=TWM$'=66%I_JE +M=8Y]&U.)R3E&S:$P`W)1@8C.%-&PAP)_,F<^.4SF&)&%&1!&5`U0^J95.]!+ +M294URYPG*KU'@CU'/99JQC]3I%TD;K%KTR)&)*W[_208"Q;W!)W]ESR=<:RK +MVAJ'PH[:AIJ`E77Y:3*%C0'3N5AO:[T^`0MRR/I$\+$LL'?"V+(@D$'A:CG6 +MI:C$85AS@UN`%6M.8(]RIJH],S"W&$55QR=E`70"#N5)4V)!5>JTB3,%79H% +M5\'A1/<'#(^O">J8YG]E!DDPJF@5@'-@B0J=S3GLKSF0"3^BJ5ORF/W6NS;+ +MNF.DPJ%<<1E:=WALK-KCU?KA--13N&!X^%F7Q@XV'ZK6K2LV\``D@(LK)O2W +M<A9%T(<2!CNMB_:",+)OB6@I9LM9]S5()@0LV_?J)$3W5^ZRPZL`<+(O'#6[ +M,]HX68XY,SJ3RQCO+S\C9<;XCJ@U<Q._^\+INN5Z8:0V,\E<5URO-0EIF#E> +MCQSEQSZ=W^"[7/ZJTN_5>N=>J"E99($!>-?@<[5>NK:X`"]!\47HJ4"P/]EZ +MO!/Y5X/U%Y57]0I%Y))W3?Q]+N5GTM)I@D$HH9V*[^S$E?/=RQH&.>.RK/TD +MF6\<*U6>X$_E^55KETB2`2N3U(@!JDX"&-1_9)Y)=O\`5($ENXQPB&<T9C/U +M0F=/?W1N)`$D9V0:N1PJ&`,;_JGQ(/"=CRUI(`AV"#E,QS2!'?9#9CET9/LB +MTZ1L1\*2N^B7@TJ98`T3J=.>3L.4S3+0[3M]D`P9V/T2$P2EK)))W]BF:X:C +MG`0/EISRFV/*69$[#ND2,`2BF&<@Q*0!W,E$8G/Z)AC9N_,H&VD$2-X3-ATY +M._=$8,<?)3:1N9@\DX4"&WMW"9TS.93M@DC8(G$`?"HB^=BCHN+'!['9;D'L +MG<1/I!CLFY)@XX0/4<ZI5<][M3GYE#'L,(RYI,B"0$VIH,Y01P7/P9/LD#P? +ME.^!$RWL4(TSOO[IH.<QD3RF'<D$I2#B-T[2'`P<CA129G)A"=AV1-(C3CV* +M3O*X)_U5`-WA/!!W"3@`2)3P)WXR@<`#U#CND#!S]D0PZ&_E&Z0WWV[J(;?M +M\IR(>,[^Z4,F&X!Q\)W@#TDS'(0,R(W@^Z3BX^WR4WHT[_3**6EH&G(Y!.55 +M$2#$-(')W09`AN?=)L3))QC9.<&<91#AW$X"N]*@U@=PJ;6D@$`1W6GTND&D +M.QE9H]`_#_-8`#CE>J=`TEC,%>5?A\0;IC2['*]7Z""UC>5X\_[.KING;@<+ +M?M22T2L/I>X&WRMVT:-(GCA<ZW%VW.!A6*8+G0H:(!SL%;MF@0866XLT&0P< +MJ1QTB04-(#Y$(+AP!@%*H:SS!(*KO=IR8E%4<#Z95>X(#42JUV^7$3A16U,% +MP=ND]NNI`,@*8Z:5/=-$A[BJ*3,X7->)/$C+)CB7[<)>*^JTZ-LXO>`!LO)/ +M$G6!>W[J;JI%.=X6+;O4>KQ>/?->A>'_`!11N+R7/G,1_L+L_P"-H5K::?(7 +M@5O6I4&MJ4ZI#VKL?#_BNG2L]-:H!A;PMG%<O-XK>8V/$76V],OA5J&&3NM[ +MH/XA].9;-#Z[!`Y*\:_$7Q"WJ5?RJ#O2WGNN0;<U!@//W5F][CI/TTRQGMV^ +MIJ'XAV%W6;;4:S7N=L`5UO0;47E`5WMW7R1^'O5FV/B*E5K/].Q/9?6'X<]< +ML[GIM)K:C2(WE+;E=9.'E\7[7]6A?='!;+1E46V]S:O],D+L:9I5&#U`SV*! +M]A3JR"``LWQ_@P\UCGK+JSZ9#7DA;O3>JLJ0-2IW'A[S:T4P/E4K_I%[TT"H +M#+0?=3^6/;M,\<G7T+AKA((A6:;Q&XA<7T[J;F0UYCY*W++J#:@`:X'X.RZ3 +M*5FXZ;3].GB56JF780,K2,$''=.XDB1"5-:15!J:>ZJW+0`3*GJ.TD@G"AKP +MYF%C8RKQYU0`LR\;(,B/=:MY3AI)P%DW67;[_HL5O%2TG5$?5/I/,(B3&-^Z +M"8=!^ZQIO:S0QMN%J]/.`0<E8]%P)Q@E:O3#!]E6:VK-WLIZ@!$E5:#HCD'L +MIG.B"MQD#Q!.57K&3A2UG3,*"I).TJJK5"2["'28D[*7TAV<E`YPV_NM1FHG +M.)&^_NJU?(CA3U7>G.55NG#20`M(IW+001NLZLT`GM[J[7<1,*C=.,F/T15. +MY=!,RLZ_@L)!A7;DDM67=O=M,@H,^NTQ(*R;W+HB/A:5Y4`$;$+*NZX)))D* +MEJA?._EF<!8M^ZF&;Y&X"UKYS7,W'U7-=4J$^81$#<K.OPY=L#Q!7,.8'3V( +M)"X^_+JI?.0<96OU^L#4>`[/"PJCCHYEQR5Z_'-.'DKT7\'!Y5HXC[_5=3UI +MI>#J)$9PLK\+K`T^@,K3EXG]5<ZS<^LM8X%TP0%Z/#.-O!YN<DEO3;Y+<\*3 +MRQW5>E5'EC40"B\UG<+>DV\%N7;-('IDY"IU6'))&#RKE8>C42T@=U4KQ,<3 +M*YO6@)B1B"F<P`'41V1.T@X(S,SPA(!F2)'=$H`)@#8<IG[_`"C'Y2.^R&`2 +M8B/<HH'!LB`?A$S0:<:I)X[)$>KO"36@/@=OE4.\$'26P0<@J>G7;3MRQE*G +M+CE[A)^D[*`-`)./F=D0D-@$`(&`],DC!2(;J<B:T?$)B,[1]4#5(:[(V3$# +M223'PDX;(@R8:(S(W4`B08#1(_1,_#>_`1%HG83O*8-$@$?7V0T89:?G,)X( +M,C'9,1C';=2.CR\B%0,Z6D#&K=)Y=`U2G#89,?FV/=(M$80,9)B4(:?>$^B1 +M@0$;8TF9S[H!#009"8-_I.(1N:(=F$+FD@#4<\H&<P1!S'NHB(=)^REJ.&F` +M[[('#83'*B@<(.^R1!=DF.,(R"'&9@]BG8T%QU:BT;P@%H+=."/A-^5^[H[J +M2&D$"?JA=$S'TA`TR?S"#WX1%S2(@`]^Z%D1S*8;Q.P0&#Z3D"<)-TDDDXW[ +MI/:)P<;Y1.:0R2[=$"V1L[8X2))F#+DW!.K=.T`.W/T[H'@D3(2`)D<$)JAU +M$Z73/!2:-,912CU8'^^Z/2(@B#V*`CTET@'@#E$,G5JB"J@K=LU/\UT%C1_D +MM,0.%B]/:75=/?E=3:TW"T)(PW("YY5J1T'@`?\`JVM$"<97KG1)\IN,PO(O +M`LBZG'SVW7KO0/\`EM)7ES_LVZ?I@@#/ZK;M1L9C&Y6/TULM!F%LV[8;.KA< +MJW%ZCF<J_0;GD^TK/H`F#(RM.S;@9E9;2%Q#-B%4J23)5FJ[$;0H'9$'"NC: +M%Q!R!LJEVZ9$[JQ<``0%4=)))2I"I-@:MH69XEZBRUMW0[*L]1O!0HDD@0O. +M_%M]<]1N#:VY)DY/99SRF,>CQ>/VKCO'WB&XN*[F4G'1/=<5_P"H=4)=J))^ +MB]1;X)-2B:M7+CG*Q;OPH^WNH#-0!Y*S/),7T,)CU'-65.N6B7[9C=/=U"PP +MYQ`[%=)5Z=</=HT&6M$``<*.MX4O:C"]M$GY*D\D^MZD<9=/<XZI,'&^ZJ5- +M1$SD;KL7>$KL/=J88!'U4'4/#%U;VVMU+*Z8^6.>>,_+D[>L^G4EKB,[KN/` +M7C_J'0WL8Y[WT@-B<A<;5I:7DZ1`*C:)$?7==;)DX98_*^EO#7XT6!#65ZP8 +MZ/ZCA=WX?_$WI=X6M;=TSJ'^(?YKXQ:7LR.V#W5SIW4;FVJ"K3K5&Z#OJ(4] +M;.JX7]/C>GZ`>&NKVUZR6O!'L5J]0H"[MM+&APXY7R9^%GXM_P`$&6_4*NGC +M43@[^Z^A/!7C_I'4K&F]M]2!C(UC_-7'/?%>?/QY8)NI]*+=1##J"S)KV57) +MP/==1<=2L[QYJ4GM+3[[K!ZZ^@X$-<TGY6+X_L,?+9PL]/ZH*@$.RM6C<AXC +M5MA<&XUK5WG-=Z>ZV.C]4%0:=0D?JL[LXKO-7F.ANJG:%&:GIR1\JLVL'4HE +M`:@.)]E*:*\+3.HX65=,TM*T*YEL<*A=G$AWT6:D4:DM)(Q[*"H\`8E'<.WQ +MD*M4J0V94;3T7CS/E;72WN(#L^ZY^W?_`#-UN])&IN^R%;5&IM`^ZD+B<'Z* +M*W`@2,J5Q:<R%4@'1F/^R@N'%H($J4D..\*"JZ'9=ONM0JMYD/DF4!?ZIVG= +M$\"<"`@=I@P<JQFHZCH:"3OPJM=V5-7C^DX/=5:T8$_Y%:16K'(RJ-V\3\*W +M7=B"0,K.OW`M(!$HBE=U0V<Q&ZRKZN`XD#=6[Z9)+C)Y61?O+FNC_LK%4.H5 +M]6RS;EY,Y)"N5PTSWE9]\XMIEJM8M9G4[EM.1J]BN3\17H-+TC2)S!6WU60! +MDX).2N.Z[>.8[:`PG(S*N$Y2ZDX875GDO<"9Y`5*@WS;UM!LET]X*>^KN=4E +MHEP)VPM#\.;=USXIIO$PS,_0KU7C';S7F[>R^&Z!LO#5*B['IR5A=4JL-P7& +M)GX6_P!8J.IV082`(Y7%=0JDDN(F-EZO%CK&1\_+^657V=2H!L2?NG_XE0_Q +M?JN6>^7DR<^Z;6>Y^ZZZ:]8X2X9',G;<JK6:7$RT;8"T[H>2#Z!K/YIV'^:J +M5:[)E[6O`P1IC[+S/4H5`X"=B@:(SOPK3G-8QX:^6NSIC*KD@B>WZ(!J%SW% +M[@.V/]^R",_F@SO*DC;4=QE"6AQ!(^@4`$&`"?@=D_IW=O\`*<S$D;93;'V/ +MU5!@!T^G_1(M!RUV)(]T-/)B?U1,(0L,"=X`GW3M$&3M[I/<"^-(`X`3O:0= +M+I!!RWE#02-+CS'9,"X8.W9$TM!!+2?JGJ5-1+G'/=%`!IW$RA?!='T4D>DD +M"4QT<S@930$3L"9&R,MD#@./.R8:"8[[IHT8RF@XGD;X2SJ`)."B8!&3PB+1 +MJS.1B4T@0SW/V3B0=XX12)TI@[!$H(W[\DE)[B223]E(`#."/=`\N`TF.\HH +M,:M\$H07$0<A.';<G9)D8!X13.U9V^J<.#1C20?NBT@R)`"8P0`<'NH@2XAT +M-/\`9(O!.70GX!D&>83>K,"1\H&:]P)(P"$W!).4^\"82()$3@I5.3@9D'A% +M4)D&2!P/[H&M`=C[IVN,^HC!W1#28QGX^48)W:0.P3:0,@X3L#9&H2#P#'ZJ +M@8(=)@E.Q[PSRPYL.()!'(_[I&(Q$#&R1$G3C3V"@)@)D'2"9WY^J1!Q(]Y' +MPF`Y..Z<CU`MB"9@H-'H30ZX)+=PNF+'MHC2L;PE0\VKS/M]5T=>GI:!N3L. +MRXY]MQ>\#S_&R-H7K_0!#6#;"\B\$M#;_3.)V^B]=\/D:6CLN&?:NKZ61H$# +M$+6MB2(_99'33D"`?JMBS:/3&WNN5;C2M&@C;]%HT@UK?=4[6=H5@XGWX4;. +M\->\F<=D-=H&>>45(-&\R%#?/`9RFH6JE5TO+0=E!5.AA<?U4X#34))^JQ/$ +M]\*%`M!S\J6Z;PQW7.^-NJ$%U.CDS$?9#X/Z4W4;FHSU.S^Z;H/2JO4KXUZC +M26S*Z\VC*%`4Z8V[+S[W=U[Y)C/6*%VVF&>6P?943TEMR\D#=;=G85:]3;=; +M-KTSRHD9"7^79N8].:Z3X48^MYCV8/9=E;^#;+^!DDZHSA6*#&TFB!GNIA=7 +M'_*\PZ2NF,QCCE<KU7']0\.T&U7`-!,[KA_Q&Z>*'3W,IXC$KV2YL:CJ+WR` +M<8*Y'Q7T(WC7`M)XPLWCITPRW>7R]U2VT5G`;?XEDUFAH&73)7K7BKP15I7! +M=3:8G:%Q_4/#-Y2.H4G.S!PNF'FDXKV98XYSAR]-Q>T3.,J1N@X!,QNM"YZ3 +M5MVZGTG"/LLRK2J-,220N\RF73SY>*PJC@&2''.)'T6MT+Q'U+IM1AM;VJS$ +MEH<8^RQ'3N1'M$)WD:,8'9:NJY^M>P^&?QDO;6W#+ISW$#=I/WW6YTK\7&W_ +M`%1E#4X,.[G'_5>!!Y`#I$D3(&ZEMKFI2>"UV1V*SZZG%<_VL+\?973>HV_4 +M>EC0YI,<+.I7KK._`+S&VZ\6\`?B(ZRMA2KU':F",G??W6[?^-FWEXTL<2'< +MA9\EEF[VY>/PY89:^/=^G7C*EN#KD0KC*HD1LN'\$]4-Q9M:7#@[_*ZNA4;I +MR5REVWE-+I=S,A5;P#08&/V1!YU3M*KWE26D?LCE5"[AI,;*@^H3G:.5+?U# +MF8*HEQ\S<%9K<7+,GS02<%=)TD^F`87/=+IEU0?JNGZ:`U@GG@++5:=-WIP4 +M-1QA,PB,[H*AR?=:VD(N[[%05W<3*3G9CA05'K4*%S\\QW*C>^!_JB,<S)5> +MX<!@G"U&*BKO.HSM[JI7J;F?U4M5Y,R#]51N'N&6XG?*J`JN`)^51NS&21]% +M-7>8D[G=9O4:@`,.CE61%;J#Y;`B?98M[6`:X=^2KMW7#3_=8O4:I>Z!WE;2 +MHJ[A$G8K(ZE5AKAJGOE7;IX;1(<[/>5SW6;EK6%PF7;DJ,=UG=;N@V3/I'9< +M5UJX#I@2"<-E;?7[II9`P`5QO5;B:CSK)/)7;";Y9SOQ1N:LO'IDCV7:?A/; +MG^.%PUD!HWWX*X>QMW7%ZRF&DS/&Z]L\*=(H]-Z*P-/K(EV9[_YKOK=F+R^3 +M+UQVGZ_?L<!3<<M^O9<CUV\ITNGN/G>H]QM_N5=\05A1N708U&%POBJZUO(! +M)R9A>SC%Y<,?:B_BJE3U^:&SPEY]3_YPJ%NXFBTM+H^`CEW=WV"Y>U_+U^D= +MGXR\-]/;3\QE,LJ'AIW7$]0Z8UCR((/*],ZXUUQU`TW?E'U4;NC6M>@T.'U7 +M.8UPGD]>WC]W1=1`U-)CF5"_2*8,[S(A>H=7\&"Z9_)<1&=ES]WX!OZ;=37A +MXY$;*;=9GC?KC3$("<SO*Z]OA"I3=_.!!(V*AN_"=<#71W/!57VCEB0!F<I2 +M#CGNKG4NG7-G4BM3<!W"J,;)+9`GNG#1SIU[X/=+T$-TM`(W=&Z?T>G&P[I, +MR<'[!4,79QQC9.(F7O,DIZE-["0]KFD\0F@3.<\(&!&0#)E,2.,RB:&YU`F= +MDTCL80(#G>$PTG`$0B@2E`T[01RH'I`!DDB>ZF)HFU`'IJ-,&,Z@?V4&S!P1 +MN40B<';.RH<@#'VSDHGZ0"21)0D@SEL>R9VEP`D;=T"D$$`1/NA><!H?,)P0 +M<')X3`$/,('80T;QE-<5"][JCX)=V$)8\N5&X@C`R/=`+LQ&D0B;#3L(GE"X +M#&1LF`':<**E!/&W*#5!+M2?<`2?9,6P"`<3L@0$\X3%N)B.(2]H'W2@MP8S +MVR@0`#`G@AA`&"A'[>Z378.<[9*!H)VPG$#!PD!P?NEIF3.RH36@8)!E2!H< +M,`F/=`W><%2-<6[8/SNH!:!D@$CY3:!S(/.$5327$M$#M,H#$D"3]4!M!([_ +M`*I])#])!!Y!3"-')?QV2+3.<(.H\&4PTS`&)(717=-C6^IO')6+X+HES0X+ +M<ZDUV@/.RX9]M1;\%TP+T$N$+UCH(_E-D$87E?@5A-ZQN"<KUKH;1Y;1,D+A +MEVTZ'IHV&J%N6#1@%8O3P0=]^ZVK!IQ$@=ESK<:UH,3(PIVRX`<*O0/V^5;H +M3IE1H4%C252N*PIU0_2UPWTNY5RZ=%,R8^%B7U:*AS"59-@OKEM.B7%WJ/9< +MK4I5NJ=3\O<3PKW5*[GU0P$$=UM>$[*G2?YM025RSYX>O#'TFVQTKHM'I_2Y +M;AP"IN:*M<ZFR"KW6+TN8VDQ^.<H^F6C0SS7%3+FZACQ-T_3K713D`2KF'&. +MW"JU;JG3)9J$!5+OJ]K;-U/JM[0L[TWVTG@-9`!D=D5&EJ."!]5P'B7\0K"R +M:[36:2/J?V7'W_XK=2)BQM2_$!SC_HF.6VYX<[-Q]!4*376Q:^X$Q.RSOXJQ +M94?2-1I=L25\U=1_$[Q@7.:ZN:378AK1_DI?"?CV\KWX9=522[NMY7C<A/TN +M7VO<.N6-I=UWFF`0=EC5/#=*H(\L$'V1]"ZD:U)KM0,A=/T<4[BHVF2/4N.U +MLN,>:^+/!U&I;.#*0'NT+R+Q)X9N;*LX%AB5]8=?Z53IT6N:X%K^.RX/Q+X< +MHW#S_+&<_"L]L:WXO+^7S1>652FZ/+/RH1:U0X^C=>T]7\$->\%M,#/`_P!% +M?Z1^'=@*7G/I^8Y@D-=^7]EUQ\UO&G3.X2;>"5:%2GAS7=LC*K.U,<=M)^Z] +MF_$;P>RM;:>FVK35:9FFWCM@+R:^M*E&X=2J4]+V&"'`@A>B>25QF,RFX73? +M56;F`<+T+HMK:LZ<7ZAJB?=>>VC8,!H!'NMNRZC4#6,U&!N)6<VKX[9.7M_X +M:W3C08V9PO1;![B!J.5Y)^$]VUP#>8WE>KV-352;F)"X8=/-Y>*O/?+("IW- +M0EN\2CK/AF<*G7<)SF5IYZI7U0P1.Z@MP7P3GV1WCB7'&$]AETD$_5*U&QTE +MD$%RZ*S_`"?V67TJB-`*UZ#2T`]ME%3,;Z!Q&Z"L1IV1>8,Y^?=15GG3GA6* +M@<<^RBJ[%&^I(Q$*M4<2[E6):=[X=&?E5JQ;$20B?4P2JUQ5):)@?"TQ45P\ +M]RJ55X;JER*XJ9+MRJ-P]V2#*H"ZK9*R>H59!`/W5B]K2>0LB^J03E:D9V@N +MZI#?4LN[<8)!W5BYJDR#.-EEW=:)DA7>F;RH=5N]!+3D+F>K7;7`AQU#D+6Z +MS6`$G\IW]ER75GZB'`B)5QA(RNNUAJ(#L@[^ZYVYESBX!IG`!5[JM8/J.:2" +M>\RJ=C1=5JZ&YU87IFI''+FNB_#OHW\5U-A<P1$_&Z]0ZC3=;64%T0(E8WX= +M=*=:6HK/;ZG#D*]XPOV,MRRI`G?*Z^#'=V\?GRW=.!\3]4TN>73O`=/^BXR\ +MN]=4OW,_HM#Q;<L-PX,?,GDK$IG^9G29Q*[9WG37CQU&K0)=2:X:A/9%![N3 +M6Y`HM!,?5'+>_P"JQMWY>K6W3!<.-2X>XU%H-Z;28T`-..96YTCI8-/43G=7 +M*]FUHPI:\#EZMI4;)8^?91:'-9_,9`_=;M>@T..<+/ZRP4[7,$\*;-;85:T_ +MC+OTLAE/<R@%FTUWAK9#$[>HNMP6MI2J5;K+V/-3RW#V"LARAZST6WNF%KV" +M7=PN(\0^%:UO4+[<2)VR?[+MZOB&B7#S*+E`_KMH]_\`,C3&Y"GK^'3'.QY7 +M6MW4ZNFH-)&(.$#6Z3(=MR%W/B2PZ=?,-2W+14W[+BKVVK6]8L<S8[PD_P!= +M\<YET9]6K4@/J/>!MJ,P@,EQ2+\>EH$)M3HB/]56B>-O5MV3AIQ!W33[!(., +M[!$V<9R8$>R4NT@H2XDF44D#;]478>9G?L40&-\?"',_ZHVF#,1[;H'8#ITB +M!_=)C!)`$^X3`N<Z`W/ZHV4JKGDMI.).ZB;@1,8@<(?ZL*=UO6:#+'`*!\ZH +M<"JNX8<QD'A`\2["?4T"8(E,]P(](,G=#87"!!'J2:V3$'/*=CF\@DI?U;%` +M0;'R$+@.\%&YS-,^W`0AS2W$_0*:-E3:/_Q#W3/(G@)PX#(D'YE`7!QW**9W +MN?LG.D-RGJ5`8@#TB)0:P3)X0/$@)0?@)]0G&R3BW$?H@-P;$S,I-.DB=O9` +M"W3$[;82U`YF(VP@>#J_*G<"3B#'NF+F@"<%$"W2<CY5#!H'])4E,'S(.!*C +M8YNTX4U)S-;9<`/=!V_@YH;:`[GN"M+J$:2"J7A,--C(``*L7[FMSJGA>:_V +M:G3:\!,+KO5F?=>J=$PUIC/LO./P\I:GDD;<KTSHX!(@B%QR[:=!8-#F#=;M +MBV6[!8UBW8@X"VK+`S]I7.MQH4&D-C!5JD1$QRJ=(G?5CW*G+@UO/U*C1NH. +MQIG98/53II.).5K/<ZH[=9'7ITZ=62=I6:Z^/'EA])HON;_(P#RNUITZ=M:# +MXW4'A'HU/RA6>-U9\0!M"F6@X"Y:O=>GVENHSF`UKD.X'"N]6ZK3LK')&`L2 +MCU!E%I).W*X?\1O$%9[#3H.=]%-Z;F'M4WBGQZRA4>RD[4Z8@'_1<9U#K_6. +MK.TAYIM=V_[+.Z;;NNKXU*HF3RNCI6S:8V(Q]UC+6+V888RZ8M#HQJ.#J[I) +MXW72]'Z=;4:`UM#BW]$UG3!>T`\\!=1:VE(V>&B8F96,;<JWY<M33C>I=,H7 +M37%M(-DKF3T?^%ZH"#&<+OKRF*3W`-()Y7$>+;Q]"\:[8`Y*WC;T87EZ]^'] +MH;JFRD'0T-G==)U.>DO8\5</&/9>0^"/';+*BUKC!;B94OBWQ_5NS.LN<-HV +MA)-37USR\65R_P`>K'Q#Y\,K5AC:2KEM9U.HTP:,OC:,KYLNO%767534HEQY +MQ*[O\(OQ;%A<ML^L![6N(]2W,;]<O+X[C/XO3[FS-$^5<42UW8A36%"DRF0X +M@`]U<ZUXCZ-UGIC*MK7HOJDRTL()`6795]>IIX4RGK7+&^^/*+J-K0\Y^FG@ +M\B%Y'^+'A9U6[-]0HQK)+H$?V7LKP"X&"8[Y5#Q#T^E>6I:Y@)`X"U,JL_AE +MN/ERM;NH5M&W&$SG%A!'!7:?B%X?-A>NK"GAQ.RXVY&C):"?NNN%]GJLFMQZ +M3^#MVYU4#)(`PO;.E5?Y`)'"\)_!>/XDQOC9>Y=.`%N.#"QCW7A_4]IKFL9P +MJWFD.)F92KDZL$2H8=&ZUIY052Y[HTX'<J[TJB9'(/"JL`+S(6OTEA=&F%+R +MU&QTVD6P3VX5]N)B3"BM!I8./JCJ')`D(L)SA\A5ZSW.$94PES<E07$`Y5@B +M+AM*KU7[G.5)4(C4#E5JAF3*K-15'G3NJU9\C.?E2U'B#PJ-T=M)*L9J"O4. +MHJG<U1!X5BN]K1G'O*SKVL)QL5J1*K7E3<+)NGSL=U9N*AEPX'*S;JIZG9$+ +M<9M4KUQ;,;+%OZT.,DRM.]?.`?N5A]5J,:TESC[*5F,?J]P\M@@QRN8Z_6;2 +MID`DK<O[F9,;#>5R'B"OYM<Z<&5UPG*Y<,BLYKZAF)/*Z#\/>E5+SJC2&RS< +MF5D6W3:CR):<G=>N_A;T5MOTQM0@A[ARNEN^(XYSUFZZBUMJ=I8-)(V7EGXM +M]3+'FFS(G/9>D^)KC^$L'DO&.Z\$\?=1-UU%X#@9*]N$]<=O!)[YN?KU75GE +MSC]$]#)AT.4#!!)@94]`MU`M])YDKGMZHV+=H\AL=NZ/2/=!;$B@T3PCU.[_ +M`**.D?45M0T4Q&,*/J-.*1(.0K]-N(4?4*8-J9WA9KP1S=.F*E4DF0J/6;8U +M(80(6U:V\`F/=4[JWKOJDM<T#X5HY>O8-&/+6)U:TA[FL!E==?VEQ@4W>HGZ +M*B[H]:"?,:2<J[9U8XFO8F"33#C[=U4JV&J"VWJ./,-PNZ_X34UD/=3(`[*I +M?V[Z3'4F:)(_I!E7AKVTX6XM'3Z:+A\"52O;'63K9QRNO=;>HSF3F6_Z*K6L +M2]^DAHWB$TU,G$U^E,V#=NRIW'3:@P&Q'.<KN:G209`U$A5Z_399#HD8W33I +M[N#JV]6G+G-/91D8,@CV7:U>E-."R6QA4KCH#:KCY8R3QNHU[1S5O;5*];12 +M:7$]EOV'ABYJTVNJ,/\`DNU\%>'+6SH.?5HZZIYA=*;5M8M92HD1R,*V:<KY +M?P\YI>#GZ-;F&&Y(!2H^'Z#20:1WB)S^Z],N[#RK1VH9/SE5K'IE%C-3F^LP +MI$][]<YT#H/3"]K'6GU)*ZRT\,]/`&FB,J3IEL&WD1$8E=)1I!N,1&W9,F)= +MUSQ\,=/J`M?;-CZK+ZQX!Z7<L,4-+CF05W(9$@#*;RR?21G98;FX\0\2?AY7 +MM230!>W<>RXWJ'2+RT>0^BX#.87U`^R;4'J:"L3Q#X5M+NB6FD,[8Y3==,<_ +ME?-;VEIS@;Y0$D#!F>R](\9^`JM#55MFN,3L"8"\^O+2M;5C3JM<TCN%9DZ2 +MR]`+2&X>'2.%$Z0<'Z(BT#(G[)B,SJ)^55"X&9+OT3C3J.N8CC>4[L`2XSW" +M"#/O/952@SB)2(.=@"EDDP=N2E!F"5-!S&H$'YGNDXX@"`G,$C(R4H,Y('8( +M&D:<8*6(.<!.0-(,C/"8`C,-A`[M0EID>R9L`?F$)5!)&!\)RXN`DM$"!"&B +M)VV4U#);)D_"KM!QLIK=L.;)[0@[[PN\LL8+1D0K5;0^L`=_94O#CW"PV`(" +MOV-(FZ$B<KSWMJ1V_@:CHI-.,Q*[_HU.3Q(7(^&;;R[9I;'==IT%I)!WG=>? +MMJ.AZ:,`&/JM>U$#(!]UG63)B``5J6W&%ATBW2$-DQ*BKO!@$A$ZH6L,B<[* +MM2FK7C@<)5BU1IC27'D;KG>M5F_\0#?Z6E=)>'R+0_\`V]UP?6;E[KXEN=., +M+.7$>KPX[=M9=5HT;)H:1JB-UG]2O/XPD`R5SEK6K5G`$D#Y6WTNAL9RIOV; +MN/HJOZ:Y[#C#ED7WAAE4Z],'.05VM-@TQ"<T6:9@9"OI*Y_N91Y77\-MLZVN +MG3@9G"HWS-+2US8/Q*]2O[:F]A!;L%Q_B7I#7-+Z8((,X7#R^.]Q[/TWGYUD +MP.E-+GAX$`+>I533`@[X61:L%$QI,K1H5-5.<_!7#"/5Y;LUX!4<97(>-.A" +M[I%U+<<+IKRLYK@!LJ3[C,O$3W6\;-[8FYT\O'2;NA<Z#J`"Z?H?1`ZBVK6> +M2#C*W+NE1J'4*<&9E2L#!18QQB<+>7DV[;W!6?1+048+`0<KFO%7AGUNN;6F +M6B3M*ZZA=0P4VF>)5_R`^U@P9R0ICDX7^-V\M\.^(NJ=!O12KU:A9.Q<5['X +M-\56W4J+7-JC5&1J7F'C?HYKZZC&Z2V<PN;\,=7O.D=5;3\UX;J$S*ZY8S*; +MC7KM]26EPVJV0<*VRDUX@E<;X$ZFV_Z?3J!Q)+05W/1]+HU''NN.-<?)CIP' +MXI](\RS?Z>"05X7UND:5RX"9E?47C^WI5>GN`'J((V7SCXUM'TNKN9W<?W77 +M&ZR;\-]L=.F_!JDYM0/)[?5>SVE0>0-UY?\`A+:Z;5K]($QO]%Z53=%./9;Q +M_+Q_J;R.J0XD\\H@TEL]E'1)>\S@?NK=)GNJ\RO0HN=5Q^RZ#I%#0T$B%1LZ +M4OGW6Q;"&QNI)^6MKU(C3[IB<2@&1GA)VV^$#/>0<%5[I^KE%7V@*K5<"2)C +MY5*&JXD$*O6=C*.K4$C.54N'DM*UME'<$`95*LXP7$XX4E=Y.Y52Y=#<A6<L +MJUW5ANDF?A9ES4()SRK-P\@N_NLZ[J``\>Q6XEJO<OP<CW67=5-),00IKVZ: +M`<B?8K-K5-1SLM6L(+BI+7%P``7/];K0,EO^BTNK5@QCX,_5<CUZ]TM=+IB4 +MDW5GY9O7;T-+F-W)[I>%^@UNI5#6<UVG>2L_IUN_J74FM@F#W7KWAFP%ITVF +MQS0T@!/)EZ_QC>$O]JY9WAUE*HQH8#GOLN\Z#:BUZ<R&_E;M*A%JVI7'>96G +M=N;0L]($$!=/T\W=O-^KSNM.%_%2_=2Z8\N]P#_L+P?J-P:UT]SA.>2O3/QA +MZPQSC0.'YDYRO*ZT$EPF">^Z^CGQ)'D\,[IPX:9C8Y$J6@X:MB1QE04CISG( +M4U$G8$D;KD[[:M&L/*;B,(_.:E0/\EOIX1R?\*;:?6]-LF..4/5`T6)(^(E3 +M`>O"BZL6_P`.&]S"S7AG;,MJ9-N9W*KW-,LIDD+5I4P:0T]E3ZE2.(Y*7LC$ +M;1+W&JX8]U'79`@">ZUC0B/A5G4QR$5C5K=[S$P-EEW5JW4=()DY*Z.ZHN/I +M`W45O92XN<`594L<Z_IX8UQT@GGG[JC6M@7D"F#$X"ZV_I,93(@#N0N?OG_S +M6L;!^%J7;-FF<+`5(B!.Z*MTBFYVD-(,296UTZWU09$QRIZK#J+6MGN4I.>7 +M*5^B:1E7?#GA^:HJ5J<P<"/A;K+=]2J)87-:03A;EG6ITP&BE$>VZFVN?K,; +M:.&&T84U&W=2;JV'*U_.81D0J]=XK.AC<`9*RK'=1?6JESR8&P'"(6S7'5L5 +MI.I:<D83,I2X''O[JIJ,RU8/XX`+HJ5!Q:#Q^JR*["VZ#N/W6_THM=3`SVE- +M<$%2M&.;*;^#@X&W"TK5D-@1'RH[JO3I#W66E;R&-9(C*I73J3<$H.I]2@$- +MQ"R;DUZX#FEQ)/"*+J%.A5U-(:00N"\;^#;2^I/JT`&O,YC"[BA87`>7/<2# +MPK(L6%N1/>5+(2V/F?Q!T:ZZ9<&G68=(Q,++R!!_5?1GBCPU9=2M7T:E(:\E +MKH&\?"\+\7=&K]'ZF^A581DQ@Y"2_*[XY>S'(+78^$,N$"20#LB,9,-2](=. +MX6V]@R9!.-TAZ20<@H\-<<80^GDJ!GD;MVE//!(^4G``CU2$B)/:!RJ'+70" +M!B,IMP9@GZH@,.]0CLF@`X(RH!9)SR$0+L@@;93M'NT)R`08*`#.W^JFMI;4 +M`/?91[GTCVGLI[8$UFD[R,RE'9=!J.;;MP(WRM[H=$U+QI(_J!/W6'X?IOKL +M`I@O($P,PNN\+VX_BV:A#@X#3O[KS^2M8NXZ-3BDQL8@0NOZ#3.(;(7,]-I_ +MECV76]`I;'(G9>:UN1OV3(;"T*8B3A4[1L08.(W5L;;_`"L5LUQ5TCYV1=+R +M^8&ZI7+R*D;B5<MG>30<\[*SMJ(O%%X*=$M#L[;KCG-%2H29"T>M7/\`$WD2 +M2`=Y]U'2IB=ESSNZ^AX<?6%TZC&\86W9PT<2JEA2(C]EI4Z8+06C;*8L^6I: +M#CJ$[>ZGJN`$3*@I@QOD(WCT223B5TCR54NH<9V!]UD=8`%)P.=X6U5I'1)[ +MK'ZO0<_N1"-8WEQ%_5-.[+'0!,J0NBC(.47BBR=3)<!)X/98_P#$5&MTF8B# +MV7BN/K;'U,,IGC+&E0BHZ=4S[*MU:V>'C0?S=UG'J8MZT-S`R"-E;/5VU'-< +M3'?W6'>8V78+BDZB1JW/"AJ5#(SC?"FO7F\IRPR&_<*G>-JMI8!!GA734_U- +M9OU5&Y]MUJMOBR*>'?!7/VCJK()'/V5FGY[7E^C$R=U>9TSG)>TO6'^8QYT@ +M3*X3Q'8-;4\YH#3JE=TYS7L=KXGZ+G/$S6N<"TG=;PRU3&?':_@W=/%G!,@1 +M$[[!>O>'#YC))7BGX4/\MI&"TD<?"]K\+$&BV(V"S)RX_J`^+@W^$/!SM\+P +M3\0:;*GB!C6B<P?NO=_&50?PS@#!*\2\16GF>)VG4YTNQ\RNF5TQ^G=K^']D +M*=BR!B`?V74D-C3MA9OA6CY5@P_].WT"U`S4[;`77#B/%Y;NI+2E+I/)W6C2 +MIPV4%E0),1]5<;3.T;>ZUVXGLVB<#"T[<"`)RJEM2@29@JVQS6CMP5`6N9$[ +M)B_?$H7GU$YRA=F/]PB[1UR""(S.ZK57"3D*6L8=`XE5;ATR`?[*I45PX<%4 +MZKY)"EN'&<'95:E4##HSR545[MXW!]QE4+BK(._,*:\=(D'?LLZZ<1SLM1$- +M=X$DG'*R[^J"QP`^JL75:6D?NL:[J$DMG'RM=,54N/4^2,;X56\K"G3($`_* +MENJ@8'.]BN=ZY?8(F.P":3M3\1=1\LG0<D0N.ZI=NN*WE@;G,*WUZ[<7RXDG +M/TRJ/1;*YNKD5-#BP$$PNLGK-M2>U]7>_A;T!FD7503,;_1>@/MJ;6P'8'9< +MIX,O!0H-IC@!='6N@ZEK!P5Y9=VV]O1EQ1VA:VY@GZRL7\4.N4NF])J.%0-< +M00T'X*H=8Z_3L:[YJB0"=_\`1>1_B=XG?UB[+!4FFTD8/N5]'])CJ7*OF?J? +MYY:C!Z_U*O?WKZI>XAQ,_$K-,$`R<=D(<2<G?W39,9]EVMW>222<)A!S$1[J +M2W$.S('N>5`S#Y))5BEE^J<!0:K':6ANO[)_,_ZRAI.)IM/MW12>WZJZ:T^N +MZ5P`_2[!E1=5J--:FTG!5VO;LJNEV"/U697LWU+\14):-@?E3?/+PZ:%%@T2 +M"J-PT.N''LM"V86TG`G8*"G2PXD*6<D4:S(DJE7#9U#9:UQ2!]BJ=2FT/,B< +M_1%4J5N7NSM*DKTFTV$#`.%:)8P>F>Y"J7+BYQ(!QL@Q^L-F2(CW7-R/XL@R +M#,1WRNCZXXMI'(F#PL7HEK1JW)J57$F<`;!:Q9R:?2K1SAD>DCLM!UJ&TPP9 +M)Y[*?IC=1\M@)^FRV[7IH%*7"7.4JSABVS&V].`V4KDT=.K+2MJM94J#2YX5 +M2C:-NJWJ9Z`5!B4Z=6M7`)=H!6E3%%K`QC8`WE;#K.C2ID,:-L*O4LV:,B"4 +M5EUF-VD(6-&DJ6[MW-?#23W4894:<Y[HJM=TX.<+:Z.T?PXU3[>ZS;H$L,XA +M:70P'4!WV*J+6IT0'!5ZM`.)+R8]U?%(D845R`QDQ/NIH8]]9TMW.`5FRL6> +M6W2,#"&HWS@2[8<*WTTZJ!#>$4%:U]`[JK5MXD8^%K.!B")Y45:GJ&`)4(Y_ +MJ-$-,F)',+S/\;^B-K]/;>TZ8ULG41]5ZQU>EI:0N5\6VS;OH-PQPDACL`9V +M6;TWC=5\W/;I,$B9Y0-(@AQCV5[K5NZAU"HPA^'$01PJ+V^H&<>RW.GH,?S` +MR/2A&6Y."G<T9]1CW3L9+9!V_54*`1!@F<%,]I!'WRD`(*(N+_S'`;`Y4"9@ +MDEH'9,9#R,&.Z<M;@ZYE,UH(_,&D($))&!"FKTZ;'!M.HVH"`Z0"(/8_"B8R +M=C(]EUWX<>!^H>)KYHI,+*,P7P/\U+=08?0^BW74KHMMZ1<UHDQE37UDZUO! +M0<S0\8(/<%?4O@[P!T_HG074Z5(`Z,GDF%X+^+5B+;QC5#,`N)CZE8F5MTF- +MW4?A.C4#)!@#<@PN\\(VN14)!G<KDO"]G_(8=4!T`E>A^&[4-MVSN0O/Y+RZ +M3IO](8?,!P5VWA^A+`2,_LN4Z'3]07==$I!M`$`SN5QR;Q6Z3-.3A*X?Z8'* +M*J0%2NGR)#MEG;8*9UW')&Y5CK;W4.GD`XC*J].!=6U$\\H/&%8LM(#MQ&ZO +M4M;\>.\I&'9ESZI<3N>ZT;9A<X"%0Z6PZ0MRU8-$[+CV^E>(L6=(M'NKU)L& +M%6HZME<H`XX]EK_CSY#HTB'$Q\IZS0)QGA6J8]$(+BCZ9WG*Z:T\][47@EL$ +M2J56AK!!VC"T7T^9F<(6TAIR03^R:1R/B6Q-5FV!V7'7UJ:50M<"TSNO5;NT +M\T&`/DK#ZIT%CS,;K'DP]NG;P^;UXKS*^Z8*M,U&$XS'99CJ#V/($QL2.%Z+ +M6Z&6N=3@D$=U7'A-KGA^D_,KA<*^AA^KQDY<WT%M1M,`G<YGD+:LNE"N2X@^ +MK)"W+7H+*8$@86MT^R;3&G$RMX^+\O-Y/U/M>&%9^'62)IQRKI\.T6TSZ=^% +MNM9#X4PIE[<@CA;F$CC?)E?K@>J=`EITLW[!<5XDZ)=4*@<X%S9WS_DO<:MF +MPMG3)YPL3KO2*3V.+VM."5F^/['?Q_J;B\^\!:V513F(_39>Q>'+AM"RSVE> +M?=%Z0:5\1387$NG&5UEKYC6:#+2,0N,WOEZ/+E,X+Q1>.KL>&C2`"%P%C:ON +MO$)>[=KI_5=]U.AIZ<XXDCGX7.>'*&GJ+ZK@)G?ZJY3=C&.4QETZCI](4K=L +MB/3'Z*]:L+GB,RJE-X>0T;+9Z31)C"[O#GVOV-,-I@$94H:"XP8]U(U@#1,= +MT3609X*TY'8W2/\`-$^8C]T1<W3G!4<@SGX3I2,<G]4#WMCB4SB)[*O6>`"` +M,H%6JX.)5.L\03'ZHGN#N?DJK<56Z2B(KBH!/;]EGW575($;_HCNJS9)G?"S +M;JX`;@\PKH#=5B)$X`6=<U23B3WE'7KR<E4>HW#:=(P86Y&;=*_4:T&>RQKR +MY$$@?)"DO+K7))^ZQKZZ9)]7^15W^&-;#U.]`:>T&%RG6+AS@XD%L;$]E<ZK +M=M=K=J.!&ZPZ[JES6\NGZN)E;G'-:F-MU%$TVW=T*8&[@)]EZ!T2UM;#I0UA +MLEN9^%SUMX??;VPN2(.\^\(>I7]RZU-(/(XPLV^]FG2X>N*]:]491Z@[2,3M +M/NK'7?%3;7ISW`^K,"?9<97J&C3+G.)>>5S'B;J=2XJ>7K,-VDK>'BWEM/)Y +M)ZGZYU^ZO[FJ35(:02<K#>_5,YY.4QD&)^@,IG.$@-Q`7M^/%KD[8&0$[RQK +M@&O+\"3$0>R8`:9#OLA;^8`$F-U`='<XV'=3T##QNH&EI@@$"5/0_/B=UH:M +M(#RQ.Z*&]D%%H\IN"<;A'I'^%RJOLL@&J9V"IVK1_%/<),E77B0\S&-U7Z1^ +M=YW,K,>&I+B&MTX!*<TXI0,)5Z3S6EIE1W#KH`Z::G`K78@9[*C4C)G[E6+V +MG>53);I`506+R^:E0_"32AK/I@$:A.RI7-TQH(:`>_*T*EE2&3)/N50O*3=6 +MFFT0J.;ZW4N*N=`T9WRKG0K)CZ8].8RFZPUK7BGB5J>%J!=5;3`QW5B5O^&N +MF-ILUEGW6U492I,U&('"*U93MK8<+-ZC<&ZJ>32&.2I2(JC?XRX@1H!^ZOT; +M>C09@!!8VH8,1`W4M;TC3,E155[0ZH2!@*"Z:`PGG97"W0P[R51NG3,\JHS+ +MIH%6#GW3TZ>)`VRB<-50XDJQ18XMC^GV**HWUN#3EI(/96/#?J:6<C"?J\V[ +M=%5A:Z)`/94_#URX7;FAIR@Z,T_*&=EF=0>Y[M+0=,[K2%.K5:2_TM[*C?-# +M7P%"*XI#RH4?3JGDW9IDX)5ZFV:4++ZAJHW#7C8%%;=1P#9(5=M:GD$B5:Z6 +M^E=VW$J.[M:;7X_109?7'-\B8V'*YSJ`\RRJ0W)!X6]U\.;2ADD'NL^C95;J +MB:;&N<7!17A'COH-?S'W%*@2"XR0-\KC*E+2Z#+?HOIR\\-U*5!S*M#6'`_F +M;*\_\5_AN;FL:MHUM.<D`0!^BF].V.?&J\?C,DR$VD:1E=]<_AS?TR22"!M' +M_99MWX(ZE287"DYP'8+7LW,I^7)P-6P/MW2<!I@?57>H6%Q9U-->DYL<PJ;@ +M-]L;%6-!QK.>>45-OI=$']$0;(&?T75_AEX2K^(^LL9I(H`C4Z-PK>$WI9_" +MSP1=>).I-<ZG%NPC6[[+ZF\"^%;/H]A3H4:8:&`9"@\`^&;/HW3:="A2:T@1 +MA=93<R@WW[KE>>7*Y;1=5IM9TU^G,-*^4?Q?/F^-JH@8,9XR5]5>(;@CH]4, +MSZ297R=XOHU*WC"NYY!)J']UCZZ>.-?PI2.BFTNYE>A].IM%```+C_!EK+VQ +M]EW=M3`8T2%PR[=&QX;I@U@/<;+M;4BG2:-H"YGPQ;RX'3]ET55NEH`Q\KCD +MZXP=>I+`%3O'@-V$]RIFN&C:52O8\P;JQ?J[T:FXF=_W5#QDX:6,`,DP95RT +MK&A;:R`/JL._KNONH`?TM]UG.ZFGJ_3X;RVGZ91@-$'LMJTMB0(V5/IU+T@Q +M_9;MDP!@$+GC-O7G=!HTM+L[^RN4:)<V3QW293&J0(5NA3C<+I(\V5/0I@LV +MDCE%6;#/RSP%+1(:T>Z50R#Q[+;A8S7L()0O:3!.`K5>GG=,&R,$I$JN:+8] +M2J7K&M.`K]P2,;\3*H/IFH\N!='*;28LZM;ZG!WU1"FW#2-NRO5J$"&E1>3[ +M&=U-JJ5*#29`,<J*JW20&E7GL)$94;+<N<)G=2UN0-"E+`1O\J>FPATDJQ0M +MM+,C[IZM/&\=D`Z`:9(^JSNJ4QY1['=6*M=P<03$+,ZM<GR2)5M;QQJY^'=[ +MTJTZA5IWIHL>?R/JQ&ZTO$PZ?==8\[ISF.9I&HTXTS*\RZK6)J:P3*Z3PG?A +MMMH>^2=LKA<I?XZ>C]OUOOM9\37&FEY;")VA9W1[5P`>`02K5S2==W9=F`M+ +MI]O``(Q*J9743=,M"\@E=%84`T"1@*M8T=#0(6C1EL#=:CRV[$`V8C"*DW.> +M43&S!3NAN%I@-0"/\E$Z&A&\Z22>5#5J>F$5#7=P"55JEP:23*.O5@02%3KU +MH;$B40-:J(*SKJX&H]D]W7P1*R[NJ2-4[85@&]KC.5F5ZQ<?S)KJOOG"S+V[ +M`;(,+<B6Z3W=T*;")"P[Z\=5<?5A/?575!!,K/NZV@:<''"?XYU7ZE=:&D#5 +M\A874KT-8XU)&.%-U2Z#"Z'`;Y.RX[Q3U@4P0'#7V:=UT\?CW2W4375R^XN? +M*I9<[Z+KO!'AT!@N+AGNO-?"G6:8ZLPW$%I<,_5>U=%O[:I8M=2<TM+>"L^> +M7>OCT>*R8[G:;J5"D+8LC`&%PGB&B*1/EM[GMA=C?7M,L<'>I<=XDKZJ;I/! +MQV6,.W3UX<QU6YHBDZ1Z@.%Q/4JC:E<EP((*V_$5PX.(;L1W7/53KGB=U[\, +M=3;Y_DYH(`R#E)P).<E("-@3"=WYL2/JNC`=OWRG8#$YVS"<@Q,8^$PD.Q.? +MHB':#M^ZL4V[$;;J$''OQE3VSL[X5&E0>11:(=MW1^8[L[[I4"XT6D(O7V1M +M]F5?3;O=(&%!TMKC3)!$%7.HLTV3S,$JG9.#;2?ZE,?KYU6J4OJ;?9&]I&(2 +MLF$-DG)4CPA%.YIEPR85-],`&8A:-9IF(5"\:0[_`%05+MS!1C)*R;V&TRYW +M/"T[W3IP?HL7JSI],S/"JL:ZEU:??'*Z_P`(6IHVGG/$2N;Z=:FXOZ=-K,DY +M7<NI"VL0V,Q`"O42\HJ[Z]S4\JF[`W*M65JRD!OJY)2Z;;AE+5&2K3*1W=LL +MA.`:TZ2846D$R1`4U:#`&WLA<TC\PW156X=B-XQA4*Y&5=KY&WV6;>.+JF@8 +M[H(J=,N?JB)1UZNEHILF5*QH92AIU/.V5:L[+0-=;+BJ*%*R#O75R>R+I=%M +M/J0`:%I5:0TQ&>,JG1IM'4&NGE23E=MFLS^6LJXI!SR3]%NFGKI<Q'=9EW1( +MJ$PHJ&VI@TX#54ZGTZI4&IHD%;%C1DR-E>;2:1D)2.8Z!3JVKWMJ,(DX5FY- +M4U-3*9/T70"U9,EH@)5J=-E.=()^$',5+$W+@:K?HMOHW2Z5O2UAH!4EM::Z +MWFG;MPM+'EC2<++4C&ZM0IN&6C[+`O+"FYQ$#/9=)?R\XY[JA<4/23"NDKG* +M_014&IH#F\J,="I`8:%U/32R2QYP<*6M:,95D9:5FPCSCQ1X$L.JVCV5+=@> +M1AP&0O"O'_@^[\/7SFOIS2=^5R^NZENWY6!XT\,6G6NG5*->DT^GE9LLNXZX +M9ZXKY2\*]%N.K]7HVE"G)>[/LOJO\*/!MMT/HU-OE@.W)]USGX0_AU2Z/U&I +M7>`XEV">`O7Z5$4F!K1@!:WLRRW=*8(MQ)&!R@LA4O[L!OY0>%!UFMYERVWH +MB9Q*ZCPMTSR:#7$03[J;33.\74*=KT&H2-F%?*GB(,K>*JU3TEH<>_=?4OXO +MW'\/T&KG=I"^4[C57ZR]Q.SID!<LKR[83AVO@JB`T:0876TZ9!$A8O@^W#+9 +MDB)726S`ZJT3A<;RZ3MTWA>D/)!`W[+6O6CRLJMX?IBG;C"N=0,49PN.3K&; +M3J0TF8`X51[Q4K_F^B5W5TT#IF?E5+)Q<\F?F5N1<>:T.HU&LM-!,X[JETRB +M7.UD`2E>N=4](SQNKW3[?32&%QSO+Z/AQU&A94\<+5M\-`5.PI>D2`M&DP1` +MW2+FFI9=LK;*9T=YY4-%F0K=('3S\+>+SY(:0=,%3-9(DIJC2,!/!;DG*TS0 +M5:9[B%$:4L_+[S*FUY@_HA.,]ME6;BK/IDB"-R@-`,W$>RLB29V^J187?V*) +MZJYH!P@Q*B?9D'MW"O4Z+FOS]T=0"0T*4TSFV8`,G*=EKZQ`C*O!AG`D!.&! +MIWV[IK:JM6CIID\K-NM9&,GNM.[>`#E9E<@$P2LY5O#%0JAVJ7=UE=8<T&!` +M*U+]Q;3)E8%ZXU'[K%NGJ\>&V3?6H=+A.1PBZ#1N/XEK1,3]U?9;.JNT`3*Z +M;P[TAK*0<6C'LN4F[PZ^3*8SE/TRR8*'J;G=7+2T'F;):2RH!,`*[8N$\+M' +M@SR34*1!5AK2$31$<RHJCBTD#A5R3C#25#4>>\!1FJ2%#4JQ*J)*M0!BIW%8 +M@;&4-Q7);^8?"HU[@Q'=5-E=W!!PJ%W7+@3C"&ZN"2086=<5_>1/":0US7,D +MG99EY=','Z(KRO`,&%D7EP)))6I"TKRN2#J.0LFZK9,';WV3W=R).8"S:U23 +M&J2[LJQLU]=%K<&"5D=3O@*;BYT$!2=1K:)#IQE<7XJZU3:XM:2#V73#Q^R6 +MR`\3]::QI&K(D`RN'ZC>5*]Q)D@\(NIW;KBI+I@]YRJ]2A79:MN'47-I/<6M +M?!@GM*]>.,CE:BUEKI`),]UTGAOQ=?=.:&.+BP<$E<T3OV^$+26@B,JW&9=K +M,[CT]*_\[4*U,EQ+7G=9/5O$5*HPZ'9(7&EPB1@)M4#G[K,\6,=+Y\K-)[ZZ +M-6K(R"JSW9D2DT^O.1[I2"0<PMN)Z=32US=+7!_^+A!J[3["43HB>/W0[R)F +M-@@8OQIS'.4^H[Y^Z1T_[RG&?4Z8[JH=IYS]2K-&"\`X]U`(@<_"FMY#R).> +MQ5&M1@4FC*/[I6[6^0V>W)1Z&?[*;:?:'60!8NGLLSI;?->`#("T_$.EO37$ +M%4?#M-K:0,J8_7@R:#0`(V1.`DGA$\B1G=(@:<E5%:I$E9]U!<9,#Y6C7])] +MEE]1=`(`^R#+Z@YK)),E85>KYCB2W`*TNIESG\85-E#55#`,NQA6*N>%:?EU +MG7+VP-Y(72VVN]J:W`AC=IY63;TVTZE.V$`'\RZ&T=3I,T,B$J)6L+3MLI*_ +M_*$X1B"W!4%5Q<=,X4$=,'6FN'0$B[2('T*@KNTMU$[(*U[5#&',$JI38(\Q +M\DHG'S:VMQP%<Z;:_P`36#B/0TX44_2[,Z?.=)[!7*Y@:0KKF!M.&P%4J":D +M`;>Z:@A#26`*C<TRRNUY&)6I3'&TJM?M.@G.%>CMJVH#K4$%5;IDF85SI0!L +MT-XR,B)4O;416C<>P5RBR20H[2F="L._EL01UG-I#\RCMJ3Z[];YTC:4=&W? +M</+W`Z6K0ITM+0&B`LUJ(A3:&`8^%4NVFC)!QVX6FZF0.RS.J.@:>Z:&:ZH7 +MOS./9*JS6R-_A2NHZQ,9Y1T$`'L56637I.8Z1A7^D5#<,\MYDH.HTP6$ +M90]#FC7$P!*"Q=6]2W,\*"Y<W0&-(DK0\27-.C8$C>,+"\+"K?76MTZ0>5G3 +M5='T2S;3M-3ADH[VIY5N]^TA:=*C%O`.0,!9'7V'0VF.5*U%/PS9&ZO?.>WG +M"[VWI>70T1$+&\.6C;>FUVDR5T$Q0D;PIIJ/)_\`Q`W?E=&J`.$EI'[+YPZ: +M[7U02`2797N?_B7NG-LW-:?Z<_<+PCP\[7U8-``),R.5PO-KT83AZEX<<T6[ +M<1[%=#TTS7;_`'6!TBGY=LW$1M/*W^@4]=9L'E<=K([GHS!_#C&3PFZV0R@< +M*?HC?Y#1F54\6U`RD1@#E8=(YSJ5P<F9*KVM=S*9(E5+^MKJ;DJ(UW0&,6 +MLOXQV\./M6_TX><\8.ZZ"TM"&3DK'\,4R6M>=B>5VMM1::(@-&-UY^Z^A;ZS +M2I:4X;C4>,J[2IG$H&L+2,`_"L,!C,$+>G*U-0:T>DA3M#1@'=5Z32!NI6!S +M7;Y6XYT;FP9,$(G`8(&$6B6Y.R"H9(`B$9TK5B0XZ1(0@@L@;GA2O;/Y1LH@ +M-+P)E&]"#2!WGE'0G,D_(34LMC*EIB0!QPC-@3JX$DIPP1$0C+=3_;A&&0-I +M[^R)I$ZE@&=E6N7.#?=7GDAA`5"Y>.5;-$FU"YJ:R1"J5`T?"NUV:C,*M78` +MTR1LN==\69U&'`MGCA8E>A#IW6S>&'23\++NWZG:0L5WQXB]X<LYJ:G9![+J +M*<4&<".5B^'&Z*8<<0M*XJ.J8V^5O&:CR>;R;IGD5'X,\JW9,=J!/"KVE/@[ +MA:5JS2((DJO/M-3:`U1U0`XRBJU"`H'O<79A/\0-2-.#"JUZL$PY'>5`UORL +MNYK@@@`>ZTSLKVN6YF2%G7%<:3!37=8Y=B>\K-N:I</3B-U8EIKFMJ?^;'.% +M0O+H#`A#?W6@&0%B7U\03.5J326I;^[&3(6+>79#LE#>7;B<9G8=U3J@EI&O +M'<IRR&I6+X("I7UPUE,D^G$RBNKC^'9ZX=I_5<IXFZN3,$@0<+>&-M+0^)^K +MM;2=H=$#$%>>]5NG7%1SG$F3DA2]:OJM9Y`<8[`K+)).1^J]F&,QCC>:=P`X +M$_=*<:9D'B=D!'J.)^J>9SO\+89S=($[;E)PDDZ<I.)D"`?C9"23/MOE$%IE +MIS'L$(B<A*`&P/U*9TEW,!11/'JQ,)LANVW=,79A,?S$`G'?E$(N;IR"2=BF +M;,X!]LI.P!$CY14ZCJ=5M0.<2PR#G=`)$',CME/`.VI7>J]6O^H4:-*[J@TZ +M,^6QM-K`V=\-`[*@V0Z)(![JB5KCI@R/E34!/)4+7#<3CLI:3FSN?\E4;EJ/ +M_3MWV[*2/]PH[8?R&R\C&R.!_P#(FF]?X^Q_$MQ3/3G2HN@U*?D@2$7BVSI' +MISGM,$]I5+H%G4\D'S2.<A3"SEX,ITW6@.$@X"3V@^K95F4[FF/\34?\20V* +MC?JM=],GN0TM6+U4AK9!'NM6[N*>@N!$+F^MWU+\C#K)]U-#/N#JJ%Q=Z1[I +M^A_S*YJ$'2W8JJYEQ6<*;6D:CL970](LQ3<VC'I&^%J<%6;*S;5)KN!#CM[* +M:W;5H5`720KUK3U,T-`@*T^U8RD)`^JR(FW#31P<E"X-U9._NJ]>@X.EAV0T +MZ[FXJCW!14M0MY.RSKVKYCO+;L-U)=73?RM,RH&-QI&7%`]I0=7J"DP>GDKI +M+2W;;VX8`H>B6;:%#(EQY6@X2PJ-2*M>?RCE4Z@(82095YXSN20J5T2!IDF5 +M4I4&R-7U073&FD3P5-;M'EQ&WNBK,UL)[)2#Z'#J43E6*](YDJKT0AM9S8V. +MRT;AL.]7=2K$=)K0R20$U*FZXJ<Z0A8QU:J&M.!N5K6ENVBV.%%D*G1:QI:, +M#E$UA&2I`W&221NFJD-9`)^4:5[AX:R3@']%D5CY]<O$84_5:Y)+&DGA16M, +M@#NFD/2:-(VD80WS&M];3A3R&"2Y9G6[VFR@X`B?9-)M#5J!]73/PHV`"Z9' +M)6+;7U6I<G1)"U.EMK7%X-4Z6JT6O$6A])M,NEQ6EX-L&T:37$1*QFT7W?5M +M,^AJ[?HMJ:=%K3@#99:BPZFW0%B]58']1IL70560#G]%S5]6/_%&9B"LZ;=3 +M96X%L-+MDGU"UCFN_53=-=JMAO$+*Z[<BC2>[4`(W1?CPO\`\2ET"7-Q!;S] +M%XYX5<:O56D.'I(R!$;KO_\`Q#]0=7N@`8D',_"X?P#;/-1U:K^7G=>?\UZL +M9_&/3[*NT6S079CE=7X/+7U&D@#A<1T>7,:!!^5WW@JU.L%IDKS^UJR.\Z/1 +M!I@Z<+G_`!^2!Z=EUO3:99;\97)>/P"3)V"-1P=S4_G<GV5WIU`U:D`@E8]R +MYQN`ULR2NK\,VYTZM..ZQY;\>O\`2X\[=%T2T+*;=+,<0MZU<ZG`,_!4/1*0 +M;2:'85^LU@!=(GN"N>,^O3E=W14BW?96:5-I;@XW5!CS/$!7K=W8;KHYU/2P +M(($*2!EW/""D,"<^RD<<;+;G]1ZCO&$+SF2/U1@-Q*BN,-#P-E&H=VV9RHW4 +MM<D1*!E36_1ME6'4W-9C9NY3M>D`8YN)Q[JRP>D2HHU1J"FI.]7J&.R%$/4R +M9",-T-R@!C\F`FJO])@E(Q8CK/R2/LJ-:2(W5MQ#B9.ZJ.#G/ALD]E+6\8B< +M#"S^HOAI,&0M&Y):V'`A9/4)<TQMW7.UVQC+O'G2=1Y5.V::UR/?=3=0+0T^ +MJ3PFZ+2.L%W&5,9MKRY>N+;M@&4H!5BW.NIL<*M1:Z!$96E84QB5UKYN]K=I +M1!9)*M1I;*%C6L:-_IRFJF>R@"L\:H)^ZK5ZH:./E-7J`'*S+NX&HYF58E-? +MW!+3GA9-:O+LE27-=VHQ^JSKBHW(QE73-I[ROS)$>ZRKVX#?ZL=D=]<-ILR= +MN5B=1NY80TA;D8M0]2NI=^;`637K%YTS`15R:CC+C(Y[(6T);!&(A3:(:C`U +MNJ<_*J7-0MIR,&)5RY#:5-V#LL3J-UIEI]..=U9RU&5UR](IG5L)S*\]\2]1 +M?5K.#2<]N5O>+NJ-\WRZ;I:9!65UWH%RSI+.JM:31(RX<9A>OQX^O;CEERYH +MO<Z>?J@?^:<=MT>`=L]Y0`QOW[KLAC$>_P`I?E;`S\%(EG<?=(``R0JAA^8P +M3('?(3.U'GY`2)S![1A(D!T#;E1=FDG8%)TET@QV3@#(!3-@NB2@88,B4B0T +MXR"E`)(2<`<3@(!(+0'$'.<IHSZ=65;K6[J5A0KBM3?3KET-:X$M((F1N-PJ +MP@0-D0),';]4IF.W9.UH!U3OA.UK1I=P>90)CHVWVA6*1(=C]5'+<F0`>RGH +M#U#V*HUK:J]M!HEN.Z/SJG_3]D%`@T@8E'([+6XV^TO%;0>F.'^PL_P[_P`L +M"9'RM/Q.#_PJH0=@L7PNZ*4#)4P^OG9]1T8P)$0HZK&/&6S*.EJ\J>.%#=7` +MITMP(5L3;-ZI19&D$_`*P+FA3IOTM9)]UJ7UT^L_2P$D<J&G:O=#W-CM*2&U +M3IE'37#RPN=L`5T5A9.\N7_F/94K2W)JA[!^5;-J7$!L`'=7OI!VU,4\%N%) +M=/UB&YCL4]2?Z5`"6`EVZRU$=0P!C"SK]S9CM^JNUZH#<C99[YJ522W`053; +MAK"XSJ5WH-K48_S:[3DX4G3K9UU<`D>@+H:5"F*`;IA/^+"H%I9C=$\>G8@' +ME0U*;Z1EFW924ZP>!J])45&]AQ`^0LN^<-8CNM>K_P`LDDK'O/\`F'4>58E3 +MVAU`"9]U:`#V2852S,F&F%?8V&^RM2*MF!2O(F)*T;SUO;39,K)NWZ;IND[E +M=!TBTEK:KSD]U*U!]/M6TJ7N5<#0/=$RF!Z3DI3&_P!U&P/.D&52O:H8UQ)B +M%9KOWW"Q>HW&I^C[HBO/F/UN[\J5CS(:P%T]DK:B:D3^5:-M2938`&B?=$4* +MEO4=3+G'3[+"Z_;,;2.9]I74=0(93);V7*]5_FG1).5-+M1Z;;AL0T">0MNU +MJ,M*!U"'.V4-E0#&C4,!,VF;R_:&Y:-RKIEJ^&K5AJ^:3))77V09Y>.%D='L +M138(PMRC1(9B%ETQB&](;;N)/RN=KVIJ%U4#(X6QUA[F4RW,E4@1Y!EN_*G% +M6M3IU;18YSA<IXTO/-!IL.85]U_IHN8/A<_U:LUE&I5J?F(P25G*Z;QCPO\` +M%*SK7G664=XSN)51EHVPLFTJ;@71F>2NLZY2:^^?7<`3Q(6#?AKZ[7'U0?C" +M\-RWV]V/4C0\-TJH8'O81)WG=>K?A_1+@WT[KSCHM>E6+*;-Y7K?@&U#*32> +M5F,]UV5*D!:ZH'RN%\?,+M1G`"]#T`6A/ML%PGCD-%&J=.S5:L>;63!5ZAQN +MO0.@6[6T6X_1<+T"GYG5"\#E>D>';9U>JRBW!)A<,KNOH>''UQ=/TNQ8ZT#M +M)(.9"H79T5BSCW76T>D5+?I[@VL-31D+D>I:?XHNR(V&RU9K289>UIJ>'#.Z +MNVT-<`-E0H/$B2/;*M4G<R5N0K2I.D2`$!/K/J_51T7P(G/RAJ.`F)E6LQ+5 +M)$004#WX#3E/3@Q.W;NHKCTF1PC4#Y9;5D#E7&D/9I=B51:YSR!J4P=!&K,* +M3A;-K)IC>(`3ADM47F%P``F$[7'(),)M-"B&2,PHS(:9*EIND$_NF>T8Y515 +M>9!`RBM7"F"YQ@]RGJ`#)/RH:QEQ`/PLUJ3:/K3FN#0QP)&Y"Q;L@$CLK]T0 +MT03!63U!T@Y"YY5WPFF3>>JY`X6KTNCZ!&25G6U#76!/!70V%(``Q$<PM^-Y +M?U66[I+0H00T\K1MJ$$.^\(+6E)D@?96B"T95KS0-5VD#*JUJ@9(DJ6Y>!B1 +M]5EW]S#3G*NDM1WEP?\`%,+)OJA+B2BN[B!(.^ZSZU0N<07+4C--<5)#A@E4 +M;ZLV)$3RCNZP:T@"7'8K#ZE=#45J1BU#U.X.HRX9X6-6+S4@9A25JCZM6&B? +MJK]I9`L#G?7*MUU$_P"L^VMI;J<,3N5*X"FWW&<K6\H4V0&[JC=4F@DF/HLR +M*R;TC0=9@%<)XXZ@VG_*H@ESA"ZSQ%7+R*%$R]QB%9\*_AY5OO\`U=ZPF3,% +M=/'J7;.5U-O(KSHM]7L#=&D3&1/9=YX*Z>[J7X77%G<4]3F@P7#?,KM>O=$M +M[6Q=0\H"!M"'P-2HNZ54M:;(;D1]UZM7BUXO+G[33YDN:;J-5S"`(,*)V@.Y +M!Y!V7:_B=X5NNG==N'T*+G43D$3C9<;4IO:8<WXE=7HQNYM'4:-P#CE#J/Y9 +MSV"<-.H"/NC#"''L.0II0`2V8^J9P&DNX]U(&^J=TY83Z=)D*B$`8TDD)0"I +M',>UWY7`=BA<QTP6GZJ`&-U/@<IH!.)RCTD&-N<I-V$C=$`&@XDXSA)P](WQ +M^B+)RV4@S68:/B%0#`??*>!(W&?=+DP9^J42WOW[H"C_`#^%/:'^9,[F5"(` +M!$J:V.0=O=4C8M@/)'K(1X_^0_9-;TM5%IU0C\G_`*_U3AO3[7Z\&CIM68(A +M<SX;K14=I&)73]6:76%03@A<;T9XI7;PX_U<*^/^UCYV7]74U*SA3C5E4KAK +MZQU$P.5-1.MD[\JK?URZ:;#'NM,@-2E2>0P!Q4U%M2Z<&Z2!W&$NF6)?#GB! +M[\K6IT]`A@&.%!#2HBF!3:/JK-.F*7J&8_1/38&C41G]DY(+3$**4SF/HJMS +M5;.<$J6H^*>2J%1VIW*`*I+]R@ITW5J@IM^I4E41#0,G[K4Z/9AK-1W/=%6> +MFVM.G2$1CW5MVT):0/3`3&<A12:!IRH:]`.RS!'NK5-LB=DY;`W164^JZF=- +M14;\ZQJ:/HM>[HZVD`Q*Q.IM?1)$F%8E3]-/IW@JS7N#^5@U%9O3ZA<W0V2Y +MW*V;.V#*8)R3OE6I&8^WK&NRJXXG9=3TNH/X5H)F%CW[99IB(5[H3@:0:3)4 +M:TU14$`[!"XR8!^Z1:(B("`@"0HJIU*IHHN,@%9%O3\VKJF0KG4B7U]`_52V +MM)K&B-E`5NS```"M,8`PD;I[>F`)_1*N-+)!.>$Z5E=9J0V))_LL$TV/K!V5 +ML=6]1(G/L%G/I"F`9(`[HS0W]44Z&EL:CA:OA&RTTP]PDG*PZ+?XSJ+6@$M" +M[GH=J&46B%2-/I]!H&0KS&CX06M.&A2O&)V*Q76,OJK`^N&G8*&[L@ZW.@YA +M3U&.==XV5C20R#F%F1IQK[8T[QS7C=8/C)F/*;@0NP\0T]%3S&[KCO$+@0YS +MG9A</+=1T\<Y>=^(_P"43F,96!6BJP%@D$?,K7\7GS*[H,">.5CVY<TY>(G= +M>2UZI&WX-M3_`!3!$97N/@FWBBTD+R/P%0\V[86[2O;_``G0TVS!"2G^M.Z) +M;;P5Y]^)#PSI]1Q/'"]!ZE#6%OLO+OQ?N!2L7:3GLF5:QFZYWP8QKW^:<">5 +MW?0JKZ->G4IG+3*X7P++[1KSSW7>=+#6,'JSW"XU].36.G=W'7VOL"`P,JN& +M2=ER-U4UO<00?=*K</?2T`[?=5M7K)*W<O:\N6.$PG"S1V&J/NK=(G3_`&5" +MF_\`57:#Q`<5N)8)U1S7<E$RL3APPA@.).84%5S61E0D7//(V"9[M;2!D\JJ +M*V)G*-E?'I^J;:TG80P@$?93_G;JW]EG^=#P8RK5.I(DX/LA9I;HZ#(..RGI +MTQ,QC]E6I2'`J:H[F8^%8Q1UH!WD'D*$N`&-@E4>(E15'2PD'=*2`JU),`J$ +M@G,IC(='[HZ/JQ$+%NW233,Z@3J*R;U[B[3_`(EN=1$'+868]@=<#G*PZ[U# +M]*MOZHF%LVM+`&TJ.RI`4Q$+1MZ0%.0NLFGSL[NBI-T#'^B&K4&DGMV*)[]+ +M2.ZS+^XT-,.581]1K[P0L>\KZOZI*>^N'/<<_JJGYR25IBJUP79$JO5>13)F +M25;KPUI61U&L&M):8A61+57J=Q!.5AW0=7?#3NI[ISJCHSGE6.GVLN!(@\%: +MZ9[5^GV#F&3![K4HT3I@&(4KJ6P@8]E(P!@+B`?=9:TK730UF7K!ZY<BG2=& +M8V6MU2XW.RJ]#Z2_J]WZAZ&E;D8RJO\`ASX9J=4ZBV]N&R`=BO7J-C2MK44V +ML'I'94/"EC3LJ8I,:!&\!=,ZFU]&7+T>/#5W7G\F7L\L\>6_\U\",+E/!\V_ +M4JE'(ER[_P`?4--8D`0N&L6AG7`T")*]&4X>?MI=1Z+9W=9[;FG3>U^\B5S7 +M7?PNZ#U"7TJ(I./_`,<#^R[2\?Y-9A?B0I[2K2B<'NL^NXN.5G3R"]_!>W-3 +M^7>5&CW`/]E4J?@T&"6WI/R/]%[57N*+CAID=E`74W3_`$G=3T_UT_=R>.4/ +MPFHTGDU*I<!MC_16[3P!TNA5`=3U'W`_R7J51@-,N!:%G.MP^J7D3'*>C/O: +MXRKX"Z-5;B@-7PLGK'X;6A8]U`"8P"O2J5,3D94SJ;'B"$F$/>O!^I>!ZUO. +MJ@<C<+(/AMNK14+FQW7T-=VE%TRT97/]9Z!:W#B13$QN,+4A^[8\<=X9H/;Z +M*Y!(C(5>Y\(W6DFE4#CM$_ZKONJ=/K650TZE,5*8YY"ITJ+`X.HO)_Z7*S%? +MW*\^NO#G4Z+=3[<Z8P05GU+>O2=%2FX+UP.8ZF&5&9_Q`S"AN.G=,KRVX:\] +ML$)JK^]^7DNSMM]@3LIZ.H.DC<Y(7?WOAGI+WS3>1[95&IX9I-.IKYC`3IJ> +M25D4`31:>_\`U(])[?\`\RZ*U\.,_AVS,J3_`,N4^[D=/:/K6^:W^&>R=PO/ +MVN\KK3Z9.-2]"K@&F1R5YWXCFCXA='*N/&;Q=XUT/GGRPUF\836%)KKW^8=O +MW5?I`<:#71+W;+6%J*3!4_J.ZW>W.=-"FP.,```*5S6LDG)4=D[^3)^Z:H35 +MJXV"BFJ/)_+LA,P,Q\*8LXC`[*"X=VQA18K7;H!]4A5V_EU&9.%([UO/8<RI +M+*@:U4..P,!!)TRT<YPJO:MVC3#&2`HK.D&,4SW9QV4:#!U8A.T$GX29ZMU- +M3I@-D@HIVCTB,%,=6RE:T`2>$M(,]D%2J`<1*R^MTP:;OW6R]H</A9O6:;?* +M,E!B="TMN2'#(VE=-0:2`5S5I2TUBZ>5O=-K.<P!VVRTS$E]3/E8^471Z;O* +MEIAREN&ZJ9(E+HIBH6QSLLM+]&I,,<(3W6EE,N'".K;@C6TY6?U2J:=$L,@E +M3;2G2'FW!<KU*EZA"AZ=3]&J1E7J(&%4'39I:)PHKH0P[_56(],C"KWDZ8D> +MZBL*^AMQDA4>K/#:&D8QPM#JE,MEVK*QJSG7-<,,C,>R1FM+P99E[Q4(.3RN +M[L*(`!"Q/"]IY=HR`%T=K3TP05:UBM4F@#9#6`\MW"E8"1)05VQ3<3V6'1ET +M\W!/NIW/8X`(K!@=4),;H[JUQJ'V4I&-U^D/X9SNP7FOB*N6U*K2=EZ=U8'R +M'@Q,=UY'^(#C3O*@:=UP\_3KXN].`\1UVU.H&F9WX4E&G0ITVET"?U4%_3F^ +MU5G`1QW5RQMO,+.>9]E\[*[>WIW'X=T:3M#M`B5Z[T$-;;M;,%>8?A];-8YA +MU8X"]3Z7`MQC/==<.&:;J[P6.',+Q+\=[TT:08#NX8GW7L_6'^EQ'9?.O_B+ +MOGLZA3IXC4#/U6<N>';P3>4=#X`J"ITNFX?HNYZ;4EK9`"\W_"R\+^E4A@X7 +MJ'A:C3N'?S-@)*YV;NH^A>)RF?&F<`JM4_Y@.!*T^M6].DT/ID@.X*QZKX=$ +MS/NK.*QW-Q:HN&_"M:Q&%1MX(F2IVB#Q"Z,)A5(."(]T-=X<))^@43@1L9"C +MU.U;*+(.K^3='T]PDSO[JO6<"S!0VCB'<J-?&A4IESM6!"LT9#=]\JDRN2\> +MD1W5NDX1M/96,W:Y;D@"5+4>-$!5Z3O2/V3U'?TCE;VP(.D=Y451X:=X33#9 +MSV4-2"9!6;6I#O?DG<*[TNDPTY<[?.5#T:R=>W0IC\NY*UNM6++"T#V&8QNL +MR6\KE9.'.^)G-94U,)(B%F68-2IJE%UBX=6J%L[&,*ST6V)`EIRLSFL^7+6. +MFA8LP,X^5=:0`FITFLI@`9]U%<U6M8<Y'NNCQH;ZJ&ATN'W7/]3N"3&J?JK' +M6+S=K2L@/=4?F8/NM=,6D22.$+R&MU$P0IQI:S.ZI7CX:8._=6,U5O*YSG"Q +M+]Y>XMU%7+VKDC.ZJ"GYE9;ZC-Y1V%MK()$_*V*%NUC,#Z)K*B`P#<A6Z3`# +M*Q:U(B=1&@.(D=E1OB`TAIA:-V\,81C98]^_5R""M8I:S;\O?5T-`+G;0N__ +M``VZ.ZATWS'M]3LY"YKPG84Z_5&OJD`#(!"]8Z;191Z>WRF8C?9=L)NN.=X4 +MNG6>FIJ(6@]C0PCE*SI/J58+H"O/MVLI0!PO1BXUYSX_I>H_"\_<P4^N4W'& +M5Z?^(-$&F2%YIUX>1<-K[@$+K>8X_6CXBIN?4I%DY@2M+I/36?PP<Z7%16KJ +M=YTVC6'8&5LV,"FUHCX6,>4G6E<6-$"?+'U5>[L*4$%FGW"U_*+Z\`>GVRI; +MJU_D$QF%K45PU\U]"OY>[95JVHM?0@@94G6+=SKO26R)S*&F*M!H:YDM[A7I +M$-6S-,ZL?11MI@&(RM*F!5IX<,]U6O;&HT%[:GZJ+M2KL83#CMPHGT*;S@82 +MJ6M<F=4J2A;5"(.Z49O5.D4KFBYKMB(B5R'6/#->A5UT&E_QB%Z!4IO9)()A +M05-3@<%!YY0Z=7HO]=J]I';*GI=.96U$5'!Q]LKMWTVU6!I;OP4%3I;20ZF" +M"-T''4>@-!Q&_(W4KNENI^GRI`Y74_P=1M3(EH.R3J0#3CZ(,&VLZ`H-&DCV +ME2?PE#L?NMIMO3+9+,^Q3_PU+_`?NIMMZY4,@8_5<%XWIZ.N-<=L2%UM1]RS +MTN$B,$+B_'-U5;U2DXC3ZAGMNKQ[1RF]5=Z#6<VOK`):/==+:UVW%(D[A9'A +MVG2?:MJ-.HD29Y*5:J^SK'3^4E=;^'*?XUV5''T9A6K6-/,CE9?3K@5,\E:# +MG!E.0<D*58*XJ:9R1]54>2[`F/=)S_,?OONE6BF(F2[A1H+6^8\4:>>\+6Z? +M1;28`&Y'95>EVX#0?ZB=UI-`:R.0%")`X-&Z0.=]T#3("-F7`1]4:'3W5EC2 +M6B3A!3IC$J<M``@;!"&.&X1#(_5,``?E)K0TG/T45'5',8^5G]5:?X=QV@+3 +M+#DJCUK_`-J8`GG*NASMF8N-,;E:[:?H#FXA9);HN`<"2M[ISM5$-$;*]QCZ +M.A4!I:7;@)^E@"]([IZE+3+H06A'\:W.5(TWAAO$+)ZZ6N(&)Y6M2&I@B<X" +MRNML/\8&$1\J6-%9,;H:1F%<I,&F8P2J%'S*,?X96I;%KJ0)*J`=(;$!5;LD +M[#;]%?J@!N(GY6;<NA[M]LJ*Q^MU=-,MC)PJ72:#JMZW`W_NCZF_S;O<D-.0 +MM3PK::Z^N-MH2,NGZ/;AENW2!C"U*+3&`%7L6AM.#@J[1TG!QA+'2)J5,%LB +M3"BO0/(*L4YV#OJJ]^#_``Y,94L:4>GP">,K18&N9&9*HVK"6<25(*CJ9VE9 +MI%3KMN!2<X@G$2O&_P`2@*5R\D1[KU[Q%=`6KAC(Y7C/XJ/;HJ.<[<1/W7#S +M3^+KX[_)PURQS[L:1K;/YE?LXIC><QA9%A=4F/\`+<[.\A:=*H'Z"W;W7RKQ +MR]_^/1?P\'F:)GY/T7I_3PUEO]%Y;^'E;2QDD!>@65Q-,2Z2[W7?&QBINID. +MIOD<+YN_\2E$"_%3,2/[KZ1N`'4C.<+P#_Q,VSO(+QGU#'W4R[CO^GO\F?\` +M@RZ;)D/QVE>O=#N3;N#VNC$+P[\&[HL:RGL/^R];Z;<>D!T_=<[Q:^E9MTEY +M>/N1J<Z8&W"S*KOYI,_=2L<30U`@M_55+AP\S;Z>Z3EC6EVA6!<!LKDMT>DK +M.L=P>/U5LDP%T8L2!PF2>-E'6>-/LH7EQ,;?"=C9(U<HNATX>Z,Y4[;<F%%3 +M(9L=E9HW.(.<Y"FB_P"!8QU,R1@*Q3J`C<YX3-(J20-Q$)FT7,=JX*K-6!5= +MIR83BKB3N%6#CJ+<A.YT!HV5J:35:TMRX?9`'ET"8E0U7[@_=14GN+Q'=9M: +MQCJ?#]-ULWSR#)X]E4\8=6?4I_P[1I`R24-'J1I6H!,:6[%8%W6=>7I$[NRE +MRUCJ,^O.Z:QH/N*\D3)W72=/H-I40,*/HMDVE1#W#*LUR=6(`3"</)Y<_:AK +MU&@D8D=EB=8N=#3Z@K?5KEM*F270?E<MU"Y=<5]+7$CLNCC;L%>L^I5,J2DR +M,F?D)4:$L!/Z(JKQ386SLG:!NG13X"R+UY<2-6.,JQ?7!((D;+,N:DB)W]UN +M33*O5ESR.ZM]/M3Z<2HJ#/4#"VNGL'DCNIE5D!3MRT`#,!,^6$P,JU5<`P25 +M1N3(,&.RS%JCU)Q+MU5I4#4N&R<?NKCJ+GD^G;:2M?POTCSZH?4'I77%C+B+ +M'A>TFYIM:S;V7H-"A%D,0LCI%E1H51`C*WZSPRV$8PO3C.7FO2.WI:&^D94D +M.+<]E7HUB3Z1(1U+@Z8(A=)IFN9\=40^@[&87F7B6V!H.&C9>J>*:M-UL^1Z +MEP'7J0?:O<NT<*@\)MU]&:R/RF%N6],,<'.$-`V)65X-`_AWL/!6EUJH*-H" +M)&,KEA^!._J="@XZ0TGNHG]3>^G+0(Y"P>FU?/O-&DD2MQENP4M.B0=UOCX* +M!J4KB[FJ`"=E:JVI%/+99"S[^DZC6EHD+0Z3U!C6-HUY@XRE@HU;-NN6%U,E +M0W5K=END5);\KJOX>TKT=3"V2JQL:8.G4/B5!R[NG78;K:9CA!0JOI5--PR/ +M==DZSBG`V5&\Z8'`@,!:>%(U6,/(J-P0<*&I1I%D`R#PKM?H;2=36EHY$H'] +M%!8-+W-([*HQ;FTAY+3[A%:$D:2#(4W4K*[M07AY>WW""Q>RHV8AP&0B'>!/ +MJ9'NJMU39O/Z+5#6N"SNH,TUH:@&C;!U(.C='_"#L%=M:8-NP[84GE!1T=G5 +M,%<1^)=,Z6U`V8(S]UVCW>@YA<OX_+7=/(WS_FKE/KCC>6-X.ZDZF^G3<[#@ +MMN]J-KO)X&RXOHD^:]S2!IF(^BZ"TKO-#)RN^7+G)ILV#=#-3#*M-N'.(8_' +MRJ?2ZAT@`_=7Z=%KQ),%9O"K#0P-U2-DUNPU;CS#EC3"H7KZC*C:+#(G.5I] +M&<TM#70(WRLJT;=C6M4PR)R@:6P(B%(2!@#=1HFCCA6*#6G`G"A9)<,;<*Y1 +M#?+!P$!`0V91S+?E1ZO7@!2@<@3E%.QN<F91$1S)2:2!M/LGW=//9%"]L4R2 +M8`69=-%:K$>EONK5]4'_`"FS+C&$5&AY=#(]2@YSJE,-NM0&!LM'H9#@#V5? +MKU,Z]1[[H_#]08`SE;G3-[;>F:4.,R(6=48:5Z"#`GE:E/+0X[1W5+JC)<'1 +MGNLUIKV;IHM(.1&0J/4ZI=?-;4;J]^58Z6X&V$G80LV_JM?UIK9D#=-<FVB: +M;:E(`-CY0T`ZD_3/I4U/_E@!&^F'4I&Z6$!6RS$K'ZS5\NFX`Y(CX6P7#06N +MQI7-=:>:MYY3<@&2IV54MZ1<=9,EQB5U7AV@&4FD\CA<]1+16:P8B-ETO2:G +MH:`-A"TD:](P(E6[8:HD*A2U/<,;*W2K-IMAQ`^JRZ1>D!H"K7IFB04S;NB3 +MFHU*L]KF2""#[I8NXBLY#<I[HCRSC,*2BWT*M?/+:9:3A<ZTY7Q=U`6]-Y=L +MW)7BWXA=9%U5?3#9:XXS\^R]-_$5TV]0AQ!C*\CZGT^G4OQ6>\D3@2O+^HRN +MM._@QF]U@6M"I4NVN8--/N976].M&^2(&&@'4J[:5(D#$MV"U>D.;YK&U7^D +M<!?/RG+V>VVWX<>ZCH.D@=UVW1;HN:TQD^ZY6VITQ3:X'!V70]&T!K8,X6I: +M.B8XN89[+R'_`,2EH7]"K/#3B#^Z];MJA\L'8+S[\=[7^)\-W0&?23/T*WE> +M-M^+C./#?PHN-/4&4W.V);O\+V?ICR:3#@[8"\%\"5C:^(RTNP'G'U7M_0[A +MKK>F=0R%C.:R?5EX=+;52*<']%#6J%U6&[3E5_XE@I_F`.ZCH5BZH07;]BI. +M$L;%K(:"T$_*NMAP'ZJA9$E@!*U^EVS'M+GDXX[KHY7A!;T"^I,'*.[I.8!( +MCLMRUL0(((@Y^$U_T[6PD)IGWFW-D$OP=U(Q@!X^%+>T#2K:3N$`@OSN%&JM +M6YTN!CV5BJ[T@1&%2`TM!:<'=3:G&C$B2M2L6!?I$GG<J%U<9'8(JKQY1&Q5 +M4M=J(;$0LVZ638JE4ET"2/W5GIU/S'YP/=5F4'/$Q(Y4[*O\,PR,0N>]UTLD +MG`NM5!2HEHW*;PQ;^;4;5<#,[K,O+G^.O6TF8$]UU70J'D6K3$8W6I-W3S^; +M/UQTTWD,MPP=EE]0O&4:3BXY'<JU>5M-)QD!<9XLZ@?4&N_WE=G@1=9ZJ:]< +MTVG?;*&PH_U/YRLCHS:E>^#GB1,KI&4PRD!$'NL[VMD@:C@QF/NLV\JAP(GE +M6>H513ENQ]BL>]K@N.DQ*Z2.=J"XK9/)F,%1T6>94!,Y.$J=)U1\1N5J].LF +MAHG$%+=$@;:T`#7#*T&4PVF2!PC93]$!"_5&D+';>E.]?K):!!04+<N&?OW5 +MZC:ZJDD21V6AT_IY>1`5VK-MK`U*C6-;DE=9T;IE2E:^EF8W6SX4\.M?IK5& +M9&<KIKBVH6UL1I`@+T>'"V[</)8XRRLZ_P#$@/!PM8VKS&MTA';Q4ZB8',*] +M<4F@PT\+O)S7&J]M;TVL@#=-7MV%A@*S3IP/=-7;+8(72,5Q_BZRFW>6[[0O +M/NJL>R@]CL`=UZKXAI3:N)W`.%Y_UNU\RC5])F"NL<,F;X*I,9YCBX%HG^R? +MQ=<MJM%*CM[(O#E,4K>JUW_4J%"D:_5'-W:#,;KECQ;3ZO>%K$-8'O'J//*U +MKNEI8=,XX4_3K<,H-],8E27,-89:/9:QBUS_`%!@<"1(C=/2M65[4.:WU`J> +MJR7N!R"I+-GE&!!!6F8KT65K?U-V&X**\K^8T5&.+7#A:/EM<9PJG4+1NG6W +M#AE(JWT^J]]JQQ,PBKO<<M=![*CT>L0UU([[JS6)TR#$%9BU%<W%=H@`%0'J +M88"*M+`W,I7M3\Q)W&RSKC4UA+<RJRLUNH6=:)=A5+CIMK=M\VVJ`/.T'E4B +M*51Q%003A0UZ52A%2A4<V#P5=;76DM6A>6N:E/S&C^I475!5O`'2)X6K:WUS +M4I-:7!W!;"L46VCW@7%#23RLW_"?Z&A3:*31JX1Z&_XBM"CTN@:32RLX-(QE +M%_PJG_\`,Y9Y::A_(3SRN0_$"H?*+)R=PNPJO#:)<<>RX#QK<"I7<W4-UTLV +MY1SG2G>34>3C4Y;G37%^ESI(VE<S<5=->0W'<+8Z-<#0)=[+K.6'5=-)V[;+ +M4IU&M:23@#*Q^FU6BFT@F85NM4):Q@_J.5*JU:-;5KNJ1O@`K2IVNBF'L.2% +M6L&C4UK0(`X6D'`0LJ@I5*K'^MN.ZO4:@J1ZOI*#0'M@Q]4!MW,'H)G=1II4 +M```=_E2^PPL^VN-(#*@,\K0H.8YHC,H"8R#G/U4]/#8RHFCU[X*E9@RBG,"% +M%=5@QIB22E=56L$\H+&@:K_.J_0=E*"Z=;D@UJDEQ.)5IS<;*1H`;M[).8"F +MFF%UVEZ"9Q\*ET,AMQ$E;'6J0=0=!("P^G'1=:"0KBSDZ>AMLHNH@&B9.45! +M[12;D!4^IUS4864Q)VE+`NGW#W$4VF<IKZAY5XRJ<DPI/#E$M$OB?=6/$;"& +M,J3@%9O<K4BU;N_DM/LI35TC,`%5+6LT6@?[<J"XK5:A)9L%JZC,!UZ[;0HN +M>TY/=<Z;G42\G+E)UZX?4KB@TZC.T*3I_3*KM#WR&[G"FTLVO^&>GNN'-JU& +MG/\`B73M;9V%`.J/:(WDPL2XZQ;=+Z<8(U,$+S#Q_P",>H7E1]"VJN8V<1*S +ME9.VL>>(]0ZYXWZ1TYKP:["1PTA>?^(_Q5#ZSFV@)'!7G)H]0O:DU/,>3DDR +M59L^@7-3/E.'T7.^61TF'Y=/2_$+JKJX=.Y[KUG\/NI5^J6%*K6.7"87@W_# +M:MM5;YK2!*]I_"FH&](I0=FQ^RZX9^T8RFK'H#&M;2WX6;UHL;;N).P4C[T, +M9I<%E=9KMKV[@'QC98M='EWXD=4)NGT6NVW*\PZ[U5[:[J=&-1W<<PNI_$^N +M6=6JLD[[C*\\ZG5=3K/>XX(P%X/-=W3V_I\>F\>)>YQ+B<GV6YT>Y>ZY:X +M;-./=<9TBX#JHU8@X`Q'Z+I["\8`-1DCL%X\YIZ;B[SI]R'4VDNR8$+KO#Y9 +MY()[+S7I-RX!CXQ[E=MX>NBZDT%PRDNT^.OI/:*<R8'NN3_%,BIT&NW?T\?! +M6LZ[T-TD[\2L?Q1_ZGI]1LXA==[B3M\R4G&T\759)`\R9^J]>\+70?94R"XD +M`+RKQ_1-CXKJ%HTC5(79_A[?-N+.F)F-\K.<W)7U,,IIZ`*NMH/ZJWTZ'502 +MTK';5TAH$GZK6Z&6EP)D9^5B=NG<=!8C8!I]EN=,MZ@RTYWT^RR[*`T;`KI. +MF-IFV!`AY'YETG;CG=1+3<:='\Q#@%-3NF&A+CQ$*A4=5=4T-!(G\P17-`,M +MYF5TE_#E<65U9X-T2,@E5<:HC*DN3+B>Z@=4@97.NLG"1E6&$DR=D+ZY#8#B +M/E0"K+S$QM*DHTC4=),#NEIZ_DF/<YQS[RIJ$:NQ4]C;AE4ZA)C"BZO5ITVA +MVSMOHL7=:FMZ7*-:FREZB``,E<[XAZA2UN93=SQRJW5>J"G3=+_JL7I51W4^ +MK-`)T3_=+>$O\>:ZCPA;&I7\Y^Q/^2[#S0R@&@[+-Z70IVMH-,`ANZK=0OO+ +MGU+IC-3E\WR9^]V;Q%U5M*FYH<N1NWONJ^K<'A-XHOO-N"UCIDI^BO$AKAQ, +MJ^WQCU^M/I5NVFP.(C"L7ER&M(!V"`U6,I`A974;L:B&D%;QC&5V:_NYD$C] +MUGL:^M5`&)/='2I/N*L@8[+5Z;9.:YIE,LM+,!=/L88#RM*E;Z&Z8YF5/;T( +M:V.%8<R&9"Y[VUI3\LZ829;N>\`*ZRE+HCZJW;6_J`T_*"O:V<1V72^&>F>9 +M5#W``*#IMGYE1K0,=EV71K-M"DT@?HNOCP]JSGEJ+5NQMM:@8!`63UBO4J4W +M!H^5LUJ>OTX`*I=1IT:5N[5!@+WS\1Y<O]871VN_B"\X([K0KO/F=_=1=+`- +M1Y`D&5.`'5H4Q^I4E`%S=TJ]/TE6*;(`$H*NQ$+I&*P.O,U6[P-X7#7].75F +MQ.Z]!ZW3BV<>X7$W0:+FJUP,96_CCDXJ_N'6=2HUN)G`*M>&:?F-\\@R3.57 +MZ_08Z_,@Y*Z+P?:4VVX:1*F?%3&?5AE9K:<.!P%!7J-,\8D3B5JW-*E!!8/A +M9UU;4RTC;L0K"L]P!=B-U/1I8"@J4*U)X+3J:%8M:S7$-?A#0].E\#`'9'5` +M=0@C=&\LQD05%</:QDS]$&'6?_"7VHGTDPK=6NVI1U-S.5G>)'!S-;-Y5#IU +M>Y?3+6C9+J4DW&N\-R7X:JM:M2:XR=DS:5>H`'NW3U+*B!ZQJGA38I7-:B>! +M"EL_+J"!D)7EC0\LPU4J5LZF^:57(&`@NNM?(JBHT0KU+16H9RY4@;A]`-D$ +MC*&UN*E"IZV$`*&FM1;IIALG"*/^IR"E>TC3!T%%_&4O\!472[U2MIH$`K@^ +MN4Q6O7@977]5JM;0).ZY&Y9I>^N2<[+KIRC&N+:F:+B2"6\!0=*<&W$.P.$] +MPZN*C]/Y">55:]S;C!@<*XI796-;32&95_IKWU;@N)PU<[T^M-O'*VNAU)8& +M\G<K5'3V0T4M<Y5RF[DR3[*A;N`IM;(GLK=`^H&,+-(O6YD`@25:9I)@JK:` +M:I"L`D/S!E9:AZE"F\2!!4;15H.D$Z58IR?=3:06YB44UE7:YL..5)=W#:;/ +M2<E4;MC:3"YF'*'I_F/J!]PTZ9VE/^B_9T'UJ@JU!(!Q*U&M],`0$%L6%@TG +M"E$`*::$T`X)PG<`&Y*>DV3V")PD^R"C?-+Z3MX7,OTT^H$KK+K\D@;^ZY7K +M32V]R2)/9)Q4O32M"^Y(IM..5IMM:;*>1J57H3`R@T@[K5()8>ZMF^R*%DT, +MN=(Q[*WUNCYG321N%6J?RKH%:<"O9EL[A9O,:G;`Z.36_EDX:CZ[<LM+<AN^ +MP4+'&RO*C!R56\NIU&_TD$M!W5[FV;WI'T#IK[NY_B:HD$SE:O6:].UMO+80 +M"!$A:#13M+04J;1('"S+BQ==-<YP.5G*Z61QW417OJSJ+0XR?NAZ?X(;6KBK +M7:3.5U%'I[+>K(`!F,K4L<;F5R_;WVU/XL3IWA"RI``4FS\+5H>'K9@@4FX] +MEL6WE@"",JVQK2TA:](UNO)_Q(L*5N_T-`@RM7\*+F:;:,F1PG_%>AZ-0V!6 +M3^&-P*75=&I3QW5,IN/4KJV+Z7R%R_B?S[2@_1+AMNNXM&"K;-/$;K'\16#: +MM-P(A7+A9'SKX\>^O?U'N$9WW7#=;#R2X&1PO3_Q=L76E>H^F/LO+ZC_`#[@ +MAP).T+Y^<YV]_@Z#T(-%4FH1)VSLMVP<VG<M]8+1F8C*S+"S+JL.,=L;(NHV +MM:B\/F9P#/\`9<,N:]'%^NKI]19J:UF3MA='T+J9IM`U9[+A^A4`RD'.<-16 +MYTFK-8,&0#NN?MJL:=Q2NGW#06DRK%7U6A8YVX63TZLT-`D"%:JUGD0W]%TE +M'CGXW='>ZN^YI-<2#Q]5RG@WKU?IMTUCCI#=]2]\ZWT:AU2B6U`TDC*\T\8_ +MAM7:Y]>T'N!G_):QS]?XY=/5AE,HZ'P_U^VZA38#5:'GW78]&K-#&D.'W7S[ +M1I]6Z-7TO%0:>#*ZKPQXVK47,96+H&#J)3+#[B]>%^/?.C515J-#CCY704JX +MIT8IN.<$+RGPSXE9<L:^E6S\KLNC]3J5ZC==01[%8EC6>-[=[TS3_#AY:-,2 +M5F=>O&"F]K#`*O65]19TUI;49$>J2N-Z_?\`G7E3089.%TO$>;#'VR-6NQD< +MJM5N!)@JK4J9&=U&YY7.UZIC%VE5.J,Y5NC<BDW.5DBX:R"@NNHT:5$N>0/A +M,=UG*1KWG5&`_F((&`L#K76&,8Y]2J,=RN7\5^*[>W+A1,N7+VESU'Q!?AFA +MY8XYB``EXYJ=3<=)4ZG6ZI?BA2RR8*[WP;T@6U-M5P`<<B53\%>%Z5G;TZM1 +MGJC)*W[RY%NT-;B"LXS?->'S>7VXC4JUPRE!VC9<AXCZE%TYC3[0KG5^J:+, +MP<A<7U&Y?7K/<Z<G"ZY7AY\9ROEOGUFOU3E:UFQE*FTD[+%Z54&C)DA7A7JU +M7>2P$SRIA/M,N>(LW=X#+&9/R@LNG5[BL'%KH/RM+H?1'O<*E5H]Y746=G1I +M,#6M'9:N=O$)AID=.Z2&4Q(`*T*%DUIVA7]#)`#83N89VA8_ZJLVFUH['LB% +M(N$$85AE(`3^Z.DT:M/Z*[-(K>C#@%=H4X(`&2DUK6>H[K4\.VKKBY#W#!6L +M>6<N&IX:L"&M>YJZ*BSTP1@)NG6X92#1B%.88W,+W^/'4>;*[0W!#:1("YKK +M]9SY:"M?JURUK"-6.RYVX<:UQ`[KK>(Y]KG2:1;;22K-%DGOE*VIAEL`,'E6 +M;>F/S'/LIC.$O9]):T#90U`(.5<+?;"@N&@C"Z1BLKJ4&@X?NN*ZG2!OGEHE +M=O?L_E.7(WC)OG'&ZU\<[VXCQ/1(N-78R5I^%:X#6Y/O*C\749:\Q$<JCX3K +M?S=#G;8^5<^F,>W77;0YD@JE4;(B<JZ1KI"56<R'_E)455#3EA'NA_A*;S,` +M?"LN8-4@*5C`!@A15!]CZ?SG[J"O9G,N)`[K5J>QW5.],TRFD8M]0I-IN@3A +M<_3J5*%_#?R$PNDK,+CN85'J-F`TO&%=<:B3B\I:(:]D@&3RIB*9;#L$")4? +M2ZK74])&6A2UF@L@%2<K>%*M3T&?U5.YH^OS&.WX`5[4`\M)P5!4!:#!,3A5 +M%>@\TZH!_+SG9:;6-JT0[3)'*H56,+)#H/*N=+J!A%-QP5FK%BE2:*8V1^4W +MV5RG2E@(B"B\D^RFFW/WU4O86Q)=C=<]U5YIWE.@X8G(6[6:ZG2-3<KFNL^9 +M<WFMN"W==OKA.BZNUH9Z&8A<[=.;JB(SRN@OZI=3#"[(P5SW4SHJ$F(2<5.X +MU.G5`VVWVPNB\+EKJ@!G&5Q?3JY#0"0/A=7X;J"=7<8RM_4EW'5:OYD`K0LW +MS`[+$H5"XR"M6R<0T+"M:U,;#`5EIU&3F%2MWD`<CM*N4':A&T<RHU%FG^5* +MK7%*GJ=":=+"=AW5-I==W.G.AOZJ">VINNJ_F/)T\`J^ZV8]H;`$;)6S0P`- +M$0IVD3)32J['.MSI=,*];N#X(*A<T5-QGNA#7T7ZOZ?9/^JTF"?;W3._+NH[ +M:J'"`I*A&(&ZBHGM$9(7-^**0;7#\Y/*Z9QEAU;;+#\5TOY0.P[J43=!=JH- +M![8RMJBWTSLL#PW4!I@?1=#2/IV6ZS&?U)I:9A6K*N&VH+B$_4:;749B%BU[ +MAS`:<X"D_#5_(>J5&UKP^6,DK4Z9:MM;85'1J=[*MT.V:]_G5`M-C?XBN*;/ +MRA9O";#;6[[FM);CX6@;1C*4"![*]96S*3!C,)ZS6DSCZ+$YY:CF^I6T5):W +MY52E+70X8707ENTM)61=T@QVQE6+I);@1A6QKTX*HV[A$?NK5!QVU*K'*?B9 +M:5JG3G."\X\,=0?8^(*;7XET$KV3Q6UE3IKVG.%XCU.@]G67O$@,=(*Y?UR; +M[CZ-\*UVW/3J9!F0I^I6VIAYE>7_`(=^,6V[&V]P^"T1)PO3;#J=M>V^L/!) +M"ZW'?,9QRUQ7D?XRV+74G0T3*\9O^EOI52ZFV9.Q7TCX_P"DF]+BT%P.87GS +MO";J]8@LQV7B\F.WHPR]7EUC8UZ3R\M!+OT1=0#C#=,M9[KNNL=!%E4TO!PN +M>ZS;LI"8E>;R8<;=\<[:PQ7K4Z;:5$?)[+5Z+6=3:"1]5F&[IMJEI_-V0UKT +M0-#H:,0O/=[>C3LNGWTU`7GT^Q706U1E:@W3$_JO/>FW!(:[4872=-OFTPV7 +M3([[*XUF\.@MB16R<!:-1M"M1T.:#/*Q:%85:6K5$[%.RZJ,<3J=`]]UTQ%? +MK?@^QOR2:329DX'^2XCQE^';J-N^M9L+"-H_[+U#IM\:K@TO`'*OW;+>YMM# +M@""IZ\[CKCY+B^;>D=4ONB]1\BJ'-T&#)*].\'^+;>X:T.<&NCD[JKXY\(4; +MNXJ.8S29D0%Y]U/I?5.C5O,87.8W&"KN9?Y7MP\TO;W@=8\VCZ:L@C@J(W8> +M8)V]UXOTCQG=6Q%.J\^G'JV"V:?CINQ(^04N-=98])JWD'!D;)S>4VLU%T%> +M5U_'#G$N8=("H5O%5_>$TJ3WDGM_V4F%[I<OP]$\1>);2V#O5+EPW6?$M[U% +MQHVVHR<`(^C>&.K]6JZZ[BUIS!7HO@OP1:V9:^I3U.[DG_-+G)QBXY>3&=N* +M\%^#.H=6KMK7FKR]].R]:\+^&+/IM$!M,!S1(6Q:6]I9T6A@#<+.ZCU`TZA# +M7XE9U]R>7/RW/B-2YN:5&EI]H@+G^IW,DOF8V6?U7J3R]I!CZJM<W9?:@B)* +M991SF.@7M9U1VAVTRJ/4&-8V0<E,^Z!P5<Z3T^KU&H`1(7/W_+7JAZ%9W-P^ +M*;2X'LNY\.>'W@!]2G!]UL^#N@T+>@V6#;>%U=&UIM:`UHA;QW8S;)TPK6P> +MUH;IP%:IV&Q=,?9:YI@&0(]D+FM;D\K<C#.-NU@P,^^R!]$<J[5J,!@E4KJY +M8S^H)H`6-F"8/=":C*1))`]U3NKS,"?99E[6N;EXHTVNSRDT5O6%0WMZVG3< +M2V<KO>@68H4FXX7+^!>EMMZ+:K_SD<KLZ%:E3;&J%[/#A]KS^3+XNF&[*AU. +M\#&&#E0=0Z@T,(&_=9-5[[A\`E>K>G'LUS7+R7.,A1=.+7W.H$#Y3U[-Q$$+ +M0Z59-I4]3FB8WA3OAG_4]-[7O+,$MY5R@W$X*KLI@NW*M-U#&WNNC)WC&57J +MC)GA3N?@M*KW)`&#NM1*S^H-!I.7(W-/_P!<XCOLNNO3_(/=<S4IZKUXC,K5 +MZ<[VYOQ51F@Z1NN.Z56-KU@:MB5Z#XEMYH.,+SCK`-&^U@[%:LW'/JO1+7^9 +M;->T[A.ZGZN,[*KX4N6W'2Z<'40%HPX[[+#:LYF2T-D<F4`8&RK<`"57JD!T +MSNFA#5``$1*HW;FD$&%<JO`$Y5&YR_:545"WU$CCA1WC-=$A712EH)P@<P$E +MKCC@HE8%H[RKPT\D']%<JZ9@*/K%`4ZPJM.`5(8JVPJ-@'V4ZIW%:YIP[6?W +M3!K7,]RIW,#FC5B$%.F`X@&$%"K3-*KOZ2KMJP8)X37#`XG&45C^:)&%*L;% +M!W\EN>$6L]T-`M\H8E%+?\*SIMS'7#_"T-'(W6/08]@-0Y:_*VO&8_D'&9X5 +M2WJ,=TP-J-`<!A=9=[<.G/7CFN>0W:>5E==93=1@CV6SU*DQO\P+%\05)MAB +M3*L*HVE336;JTXSA=5X<J>EW^:XUSBQ[3'W70^'ZY#)X/*WWRQU7963]39!V +M6ST]P(&#E<MTNL=/YACW71=-JCR@0%&FS1=ZA&5H42-.^5DT'ZS(!$J\*P;3 +M+CVV6=+M)=UG/<*#'23N95ZRHBE2`@@\K/Z8T%YKOW=LM:@X$"3A3_524XVD +MR5,`(B%$V-X.RE9"*DIMB"5+`<R`HP/3,J1IG$J*A+32=K9LK%O4:YH)&2DU +MH."9)45>D6NUL)]PBIR)S&/=9OB6GKM25?H5P6AKL%!U5GF6C@"I2,#PP\A^ +M@KJ;=PB97(]#>&WSV.X.RW[F[93H:0X2>RVST;KO4`QOEL,E8_3Z56]N(&W= +M2/8ZXJ:1))Y6Q94*=C:`F-<96=G8+RX;:4VVU*#4/9;'1*7E6X?4&2-UC],Z +M<ZOU$W54SR`2ND`:VAI[8PN.5VMA6W4:+ZYI!XD<2K#R'`&?C*Y#K6JPN?/I +MOPXY6ST'J3+F@PEV2%TQU8U_5I56`M@Y6;?6[2"M34'>D$&5#>-:*>8(2QIS +ME;^6X[@!1MO=!C)6C<6KJ[SL`HSTQ@&1*SLTSKBJ^Y)9_2N'\9=&+*AJTF3. +M\+T6I:-9^4`:55N;!E5A%2,K&<]N6L;IXU7IU*;I`(A;OACQ3==.J-94>2S9 +M=/USPO2JM)I0#[+C.L]&KVE8RWTM*QCG<6[C,GI?0_%%EU!@8]PF,_[A;0MK +M.K3-1L`N"\-M*M>UK:V/+7!=#TGQ?=4G>75>=/O_`-ET_CFQSCTWO'?1YHOK +M"!"\?\4U88^F<0=Y7J/5?$'_`!"R\L.ES@N&ZIT2K<5'.#=1)PO+YO'KAW\7 +MDWV\SJ>8ZLY\&3M"EL];ZKJ;@`!GY787GA_RO4YD+&N.G,I7&L-+?A>:XZCU +M3RRHQ7=;T#/S`1]-ZG4J7`;J,3D2J'4V.<[,CY0V3/*)>[Z<+EZZCK-6.^Z7 +M?@4@TG/`*UZ%1M2D)$D[A>?],OG/N@ULS\G9==0J.IV8<(EWNM2\.=XK>MPQ +MFI[2/H5:97]49C]UA=*K.JB,GYY5FI=EC\DAW8JR+[).IU`XF0-^5E=7Z1;7 +ME(L<P>K*DZK=--,&23\IK*O4J.$\>ZQE_KI*X[K7X=LN-3Z(ATJI2_#6NZD& +MAH:!S.Z];MS3%LQK@)(S*FHUJ+1,1"LEZVZ3S61X_5\"%CJ=)]-H:S^H#+OE +M=3X=\&6=HYM3R?4-B0NHJ%E2Z!#>58K564P,PI9;Q:7RVK]E:6MK:M+:8!(Y +M2-^QC/Y9,MX"R;SJ;74M,RJHNV`Q*MLG#G.>URYZH75C)52[NP\ZM655O&:S +M+"<YCLLZM3>&%KB09Q*X^_QK2Q<D5-0+S*JUKC0R`X]E4N+MU(QW"CI5C7:' +M05F_E6C86C[CU9C==YX&HLIM;+0>ZY;PM2\QH;J))7:]`L:E(X^JU).TMXT[ +M'IU2F&`#$J[YX#=\++Z?:52P1)6I9](NZ[L@@+K+\<D52]$F"H*MP\MELF5O +MT?#C6MFJ8A&_IMO281HSPNL\.>7QB^3&.2J&XJ.@`Y3#IM>J9=NNG%I2!.EH +M1,H-G\L+>/Z>WMB^;\,&UZ&TP7"?E:-CT2D'`BGMW"V+>BR>,*[;AH'$+T8_ +MI\8Y9>6U4M+(TV@-=MV4E>W>6@-*O4V:L@Q[*5C`)E=ICKIC;#=T^JXYRAJ4 +M*MN=0^RZ-K`L_J3FG`[K4P9M8E2\J.=!D`=@H.J>)[?I]-IJU`/;_82\075. +MSMG/YX7E'B^[N^HO>:;"6@K>YC/;)PSMZQ>DV?X@]+J5=)KM!^O^2Z#I_7K> +M]IAU&H#.<%?*U6G5J7]2F7O8YIG!73^#_$'4>D5!3J/+V#:5G]W&W34E?1_\ +M4',`)RHZE8/V,KB/"OBVVZBQM-]0!T97327MUT7RU=$V.]>=!@A8%)Y_CG$@ +M22KO4KE[00X$++L:[7WD#E6])]-X@;-$X^B\U\5TCYSH:!!X7J75!KHG'T7G +MWBVVRXYP5J.=['^'%V^7473@?W78R8_U7G/@ZLZWZL!,:L'*]!+R:8(^\[K' +M^-"J.`;D*E<D:L%25GD#=5JIY*NA'6+B,C?@*![1&0IBX.RW<;!!IDYWY01! +MQ@B,)XU-@%3>3Z9E`69G909O5:6J@[.%1Z82&.89QMA:O48-$\@<+&L"16>$ +MO42?5DAAV'R(2:P`AP">F`XR1A3M8W3@04%&YSB)16M,@SIW4KZ<UM)D?"FI +M4BR6G'92D6*#1Y39(E%#>X2;HC;]4O3V_59TZ.:94'4>JN8XC0P_=#U6A2I5 +M2`0.T*#I3?(NJM7,%1]7>XN:^<NS,KMJ<//]9G4*9`QD%8'5Z(#20=MPNGNO +M5;SB=Y6!=L<*CIR2=DJN:NJC)]6Q.RV/#]0TZ,D@SD_=9'6&:+B>"5?Z0X:0 +MP5/:>ZU.F:ZBP?',+H>DW$4XQ\KDK*J`X;SW*W>G58@DXC9(O;KK!P(&=QDJ +M6O4#M-!I!)W6=T^J74QZN%/T]X?=NJ$&!LI81OV+6M8!V5ZG^2(*SK)PF!`5 +MZF^!NHTM429W*EIB8[*O3<)B=U9;@"%%3,_+`W4C6X'WE14\22I9$1.%%/F, +M#/RBI@_U94-2LVGDN`4)OJ0$`?571M8KVQ)U,*@KUBRF14G'*=G46:=L+/ZK +M<NN&:*;9E/4VPK:JYO5ZK@#"T#7?7N!3&RDL.FM:34K'?DJ*Y=3;<AEL-3AV +M4EX9URW["A3I4\Y="N4[2I49KJ?EWA#X=LGZ&UJ_(E;59H%.`V`LWGIN37:C +MTUA!(C'"NENH22`%%0`8X@_='4JRW2"#*QC-W:3MG=5Z>+UODM@CNL=UG<]) +MKB"[RUV/3Z$`N</S)NI6=.YH%K@#'W4LU?:.LFYJJ/3;ME2B"")[*2X(JPT; +M$K%(JV%=S7M.DGW6CTVX:^H"3LNDLRG#.M7E?I46,IP<J&XI`;`*SJ!`A/ID +M=Y4TTRZ](&<9[JM5ID&2`MA]$OVG*J5J,&#/LH,]S/0!&#RJ?4NDT+AGJ:(* +MU:U.)$%`YL".`LW&7M97%]8\*T:C"YC1/LN.ZQT6O:53#,-)7L7EEPP)"S.K +M]+94!=IB=UROCUTW,M]O)J3:E%^DL,!;_1ZC'LBH`#O*Z#_@%.LXZFJG>>'Z +MM&KJI20F[]2R?%&_Z?0N*9.D+E^K=!8ZF][6R`<GLNW?9W%)H#ME5O*#VV[Z +M5,"'X=A8N,R[:F6GDO4^E>K2UNTJK=='<:<AI!7=WO2*QJ'33+IWPH+NV<RE +MIJ4B#W(7&>&?EV_=KA[*W=1J@D!L8GNM6ZZEHH"'Y'=:0L*=1I&`%G]5Z:#3 +M);&3O"YY>*QN>297E?Z)>NI40YQ;!&X4M[=MK/:01)]]ED68<RB*;C):,[J& +MO5J4ZX:""TXP5PO'#K&C4J^:[3J,3O*N6E44-.?JL>G5`:'2`!O/*EI5W5'[ +MRUJQMN.CI7CG$MG"G_B(GU3.ZR*%9E%AAPF%4N;\@N>"M^Q&ZRZ:VO@SW17E +M?73UMW/'9<C_`![G7,AYS[K09?.--H:<_NI<MQ=:37-4AQ<#OPJ]*N[SR2X^ +MR@N*SG"2`0/=5*M<"H2TG*YMR.@L+L.J-:YVVZ;KU;R6BHT$25SM*^%*LQY= +M$')!70731=V!<001LDE6\=L*XJ><]KFY]N5IV5`LM]>K$8PL[I]J\7D%OY<# +M*ZNTL:E2ES'*LPW>&<LI(+PV'4ZP=.^R]6\%61NV!SMB/\UR/AGPW5K!L-7I +M_@WIE>SI!KFQ`V^Z]7B\4MU7G\GDNN&_TKIE*E3:"`5JQ1H4Y;I4%K3?H#LA +M!>M<&Y)7OP\>./3S7*U6N[MU1Y:W;N%4KU''!XW*FH#+DSK6K5K:H,+>^&5< +M9,P$3`XG`6C0Z<XMET_57:'3F-W&>RD&71I.DR,*W3MW2M-EDT$G=2MMVZ<? +M9:D%!E)P;G*)C#VA:'DC3(2%$1(51FW>IE/98=[<>6"3]ET'5"&4\[E<CXAJ +M[M89DK7QBN7\47=2YK.IC8'[)>'NGVE>V=J:-495^WZ9K::CQ^9+H-D:5Y4` +M/I/"X>3^4W6<IIR'5/`M.OU5]>B(U'98G7/#%U9G#-0!WA>ULLAIU-/Z*M=] +M/;5:6/:#VE9GBUS&Y>-/":1K6=>6RQS5VW@OQFZ@YEO>.G&Y4GC7PN&36H,@ +M@SA<!U!IH52UP+7-,;K>&5Q,IM[O5JVO5;'72(DB5R5RVM9=3)$Q*X_PCXMK +M=,K>35J33(@2=EWEA=VW5XJ4RUQB=_\`?9=]RS;EK5%_Q!M1FEXAWV7.^)F- +MJ,>=,@G=;/7K(T3YC20?98EW7<:#V.`U'&1LND<LHY*UJ?P_4@X8@XRN_MZQ +MJV;'-=DCNO.NI_R[N1O*ZGP_=N=9MU'("EG*R\-USCR?LH*CAM.$#'^8-SE( +MR'&(4:`0!ZN5+3F-DWECVD^ZD9@94#U!#1F57KN]/8\*U5;Z0[,JA7=G?<]U +M8BM=D&GI)_18ML?+O7C43/9;%T<.))E8E0D7Q[)>D:=(2V=X[*3;`5>W=``" +MLTFF>9]U%-1:#5]0(A3/`!E$QL"2<]D-P0,_HHJ6F\!@&4_F#W4+3Z1ZH]D\ +M_P#6HUIS=9WH;3(`,969U!AT$QE2U;DFLZH1`.TJM<UBYLR"5VTX(:A(H_F^ +MBS;RF7DN=PKM=SM!XGA4ZHFE,PI5<MUUI;7#G3I!P0=DW2ZCVB7MD'^J5<ZQ +M2Q,3RJ5H#5I.#I`&V=EJ=,ULT*Q#P2[!'!716%4:6DE<9:5=/I+7XY*Z+IUP +M7TFB=MH*I+RZNA<%E(0Z0!PM?I`TVV3NN5H5XIM'O]UTE@_^0WX4L5NV3\9S +M]5I6[NZP[6J`X0?JM2SJ:FR)[Y4I&A2_.3E66.TA5:3I8`%-3)U1*C46J9Q( +M0WEVRA1DD!)BYS\0J%[5LV_P9(/LL7*8\UJ3?#8I5*%4:GU9!15KNPH`RYI7 +MF-!_B*D-&A_;A07=MX@N`Z7.SVA7W\?Y/V\^GH%]XAZ52!UO:`-LE8UYXZZ; +M2D4(<?:?\ES'2/P\ZO?U/.O*[P#F%UG1/PVM*+PZOZOF?\UC]Z7^N+7[6O[5 +MF#Q/?]1J1286TW<E=KX*M*9#:CQJ<X<I7G0+2TZ=_)IAL)>&:S:;M$P1A:QM +MR_LSE).G;68``;$1PIZT"F52LGZF`RI[JJ'-#1E,NEVJ5ZFFF23NI.E4C4=J +M>=ME'6HZR)..RO\`3@&P,=LK&*XS2]28(P43FM.WZI,&(!!^JD:.\?=:;C(Z +MYTYMQ2/^+VP%SEN]UG<EE21P%V]0!P(,$+"\1=+;4I&HQN1V7/\`K=Q;-\4U +MI=,<T>K=7*+VD#..,KF;-[J=4TW...Y6Q;O)8,KKVQTTP6G8P.RCJL;"@IBI +M.ZE.N((6:T@K4L3&ZJNIGX6B[20,;J)P;R/NH*@!&!A#6IMJ@#WW5I],3,?9 +M`8;N84[X7I49;,INGGNF%!M5T$81N>:C](`5R@QK&#W4OX(R.H=/%1D:<+'J +M]/'FZ=/LNV\K53P%5J6+7/U1NL7%K;F[;HM$PXLRL[Q'X=HUJ9T4Q)W@+O:- +MG@`C"CO;!I&V%+A%V\AN/"M1C"6@_*Y?K?3*M'4T3\+W>ZLV"@06CZKS/QI0 +M\JY=%,@$]EBX-3)Y6\/9=%A#AGE-6B"8,CLNDONF-J.+].%B7ED6/+73'[KQ +MY^/5>G'/;+\XNJN9I<X*Y2,-D8CA3-M&ADEH'>!NJX8]I=`7++#3I,]E5N7; +M!Q'<JA?W%1KBQI)*G;3>*IEI=K*LML*;FESV$D]UC4CK+(R+8/#C4)]+LD+6 +MIUM&F3Z>Z"[I4J,`"&G@*(.<ZEIF,)9M;EM9K7-)M$^H@E9#*IJ7!!=`'8*V +M+2I48&.;^;F5:L.F-#=9:1ODJS$]Y(:QM&U"Q]29WA=7TI](46L.\05D6-J' +M.#6%=7X:Z,0W6[)/9;PP<<\_R'IO2:=2Y#V,D'*[#H_098':-ALM7PKT9C0/ +M0<CE=OT7I3&LDMX7J\?B<,\]L;PG;-HU-#F1A=I9TF!H(`677L/+K:J8`6G8 +MM+:8D_*]6.,G#BM&H6F&C`5;J#I&1'PIG.;JPJ?4GM#9#I]ETTSM6K5*=&GY +MCR%-T[K=C&EU1I(]PN+\:]5N:5`TZ3'?(E><OZIU6E<N+*M2"=I*7+#'BIK* +M]/HAO6K,`$5&?=/_`,=LA_\`I&_=?/K.M]>B-50_=%5ZGXAB07Q]4_<\9Z9O +M?CXBL6X\YN?^I1N\46`.:K?NOGQW4NM:CYCJP^Z.C==1?E]6H([DJ_NX)<,W +MT+1\1V53`K-^ZF/6+73+7A?/G3KCJ8KRVN\`^ZWJ?5+UC`U]5T_*OOA4UE'I +M?6^L4G-TM=)'995"B;EQ>X$MW7/^'?/O*\U2=*ZI[A0H!C#^JS<O>\=+)Z]J +MM^\,864QD*'HC-%4ZOS%6J-L:KBYQGE2T:(:\F!]%G/IG2_;N](DRCTMDF<% +M0T8$P1*DDQD[+>/,%>_MJ55A8YLA>??B#X,IW=-U6U;I?/'T7HU0@C;*JW+` +MX$`*Y8S)9=/FCK?3[VQK&C589:2%K?A_XDK=*Z@UM;46$1GC_<KU#QKX8H]0 +MIN>U@U[X7E?6NBU>GW+@]A`G=<;[8UOC)[(:E#JW3&U*3VY$X*Y#Q#:/HM=$ +MS.ZS_P`.^O/L[@6E<S3(ALE=EXIM&W-AYU$#(7IPRV\^>.GDW52YU7,`@[K9 +M\*LJ7%N=#H@+.ZY1ASLPX'NM/\.G#SC3C!5RK.+7:+BD0"?96*3*[@,`+5K6 +M8.0`HV6[V]L;++2@:-P)!<$#FW#,3*U7"&B?U4%9NF85&;6J7.VP"S[JM7!] +M;/LM>Y,.R`53N2UXG2$1E?QF/5QW6=4J-=?AS7<E;%U18_\`ISW6'<6T7GI/ +MT5O236VO;N&K>)Y5VAIF)^BPPRNS.DJS:7548<W'S*RK<C21G"@N'`.V*K.O +MV@:7#]$GW%)P!F.R"VP`M">&J%CVZ!Z@G\QO<*-.$NZL^GA.`/+!=E1U634P +M/=2L:U]/2.%V<5:H095:J=0CG967`ZCC95ZS03'ZJ:5E]8I`T-``E9UFP`.: +M0/HM/JC9JM9V"IT*(:\P))25FHJC21#A\:5=L:CJ+@W8#@R%7SKCCOPK)IEU +M-N-7N$&O85IK-DF"NJL:HAN9PN#L*FF[%,SA=/T^O#1"U.5=/:UB7!H^RV;* +MI#<?J5RO3J\O,N)6]TVKKC&R6(W*-0``!6K=S7.D+-H.DB/W6@R&@`?HLUJ5 +M<:X<HJC6ED$`J"G!(_NBN*PIT_[J:VNT%X;=@@,"CL;1E:KK<T`)4*+KE^M^ +M!P%IV[(`;VV"QJ7IKI:M&-:T-T@*WH]/^JKVXSMLK;!_W6A4ZC0\RU(7+V+1 +M;]1<TCE=F]DM(7(=<I>1U`5`8DY69=9+>G4VEPQM)L<C96[`&JXE<_T>J;D1 +M&RZ;I5(LI9Y4SO.G.<I:K`&>Z&V)#O4%/5&H?Z*!PAPSE633JT:)$-RI9RJU +MLZ6@&`58:"8[(T51H&905F![()!"F+0/<CN5&1@D`945S'B#IQIN-9@(^%5Z +M==%I`<2/8KJKJF*C-+A([+E.OV+[>IYM,&%B7UO^+9MM6M5KP,R2.%;`$9Y7 +M,](O@3I(R-UMT*NH`@[KK6)5M]-I]E&ZU!R$;*DP.49]+20<]EC4K6V=>L-, +M8*I-IU:C@'#'>5HO'FU2(Y[JRVB`T#3"S_D5FV])E-V<E66,&L$%2U*()QNA +M-(@X36A("`I:36P2>55<2",8[J1E8!%70UH;`E#4:"S*A%6=M_E.'M(R<H*M +MS;![B-*Y?Q?T*G7H.<&Y/^^RZVK4`<51ZE4%1FF-UC+':[>54>A52]S(Q*Y[ +MQ7X?NJ)+@TP3_OA>P4NFN;5-7RR0[L%'U3I-.[9#J6X[+A<-NLRT\";1J4_3 +M4$_*MV?3&5R-`A>B^(_"5%E$O:T`_94/#/0CYI#FX"Q<-\6+[:YC`M_"DT-? +MUCLLWJ/3S0FF*9G9>K'IS@T4VTXGE"/#--_JJ,U'E+XI.B>2WMXG<]'N*KQ_ +M*<0I+3H=Q(:6GY*]J'A^V;Z13_15+SPZ)!8T1\+$\<VW^Y7F+NAOIM#N5/;] +M-<ZG!/U7?O\`#-6J(`PK%OX4,1IRM?MQ/>N#Z'T>+F2["[CI%G2I:-3H5YGA +M&L!+"1[PM&T\)W3J0ESI[PM8R8LY6UK]!JVK&CU`'NNCHWM!K``]H7`7W0NI +M6DF@]Q473K;KSW'4XPO1/)XYVY^N5=U>=2MV/!+Q]%/2ZM;>0#J'W7#5>C]6 +MKN#B]TCLIZ'0^JD:75'1]4_=PV>F3H[_`*]0H_\`Z1L?*CL+\WSBX&0L^S\+ +MU:@FKJ=\K>Z7T06S!N,*_N;XD/37U%>V%K<VY#J8<3W"P3X-MZU<O\IH;VC_ +M`$7;T;-K0"3)/NK#:=-K8`!4R\<R673E+3PA9,:)I-D>P_R5O_RY9-;'D-(^ +M`N@JQ*@KN<!NK/#BERKGJWAJQ+3_`"69]@LOJ'A>U+8ITQ]%U^E]3$84M*U! +M;D9*7Q8_#VKSEWA]U(D"F`HO_+UQ5J`Y#5Z3<65-PF(*SKLMH'3`,]E/V[\I +M[,KI=HVQH``>H"%;HT'U'R9,J6A2=6=J<,%:-O1#0!"WC-349O/**WHPS+8^ +MJC<P"K!G*O\`EC3@*C=RVMR,K>N&:8M+78S*1,$J:MFB#'U55\SE2<5DG.TG +MY4535F"C)$@'"9\&8*TJK5;JF2N;\3]"HW])PT#5W74%A)V0OI:FQI4N,IMX +M7UGI]QTN])@B#Z2N\\!=7;U'I9MJSO6T`>I:7CGH-.\LR]K1K&<+S[P]<U^D +M]:\IQ+6DP=USG\:MGM%OQYTS^%K/>UITN."%E>`JOE=4+"XYXE>@>)[6GU/H +M@JL&0)QRO-^AAUKXA@-YA>B_U<9V]4I`.IC!P.2HW,R5-8Q4MV$DB0AK@#(4 +MBJ58.S.P4%9TM^5;J@0>52KG!C"(I7,D20J+R-6ZO5RTX"I730(`PJB"X.#( +MGLLDLF[U`+3N7.GV673,7I(VY4O5)VO,$S_DIZ+!AT#"CI`;S$\J>FT3@J*" +MM1IO&VRK5;5IR'*XX$-R@=!,CA-$0LM:VD14PB_A:_\`\BMTP-`S">!_B4:< +M"XESH."$=$D3!D!0L)#<R/D*Q;Z@V8&Z[:<:BKR'%5JC0'YY[*U=CU856X(` +M)D2%"LZX:7W3G#@PH:K);`P?E:'E11G:52JL(>2,SA155K)'<'LK-`Z</&^P +MF%"YI#L_]U/0=K<,25>TZ)S1_$-)D&-PM.QK.:((.>5E7NJE4:YAF3$'A7K" +MIKPX3"16]87!'($KH^DUM%,$'=<10JNI51.&KH^B7(>T'5*TS766+Y=RM:W< +MV,RL#IM4$3NM:V>20I2-$U64J9<H+1C[NOJ(A@]U6K$UZXHM)CF%L6K12IM: +MP<+%_#<_*9C`Q@#>$QD&0-^Q4K#QV35#`C)*:5-:N.L$G`V5ZFXG/<K.MW28 +M5Z@9$;*BP(+8`*YSQC:N>&^4W<KI*8_EX,*I>L#ZC01)W6,HTH^&+-U"V;K! +MD[KI:+=#`J=NP`-``$*9]R&D-E8QYK&.EID'!450#5A/1JM)B4540<+I72#M +MR!LK=%V),+/8=.T^ZN4G`MXPBQ+(WWE(Q_LI-!/;'9"T0XP5&C.`TD2J74*- +M.I1+7<A6W&-S/LJEZ?4`.<%2P<?=6=:SNW56DZ25H],NPX`ZMEN7=FRM;:2! +MLN5ZA:U;*O+0=)*SC=<4RF^736U8.&_"EKU(9A8/3+T.:!^ZTC6;4<!.5NLR +MM"RICR]4F5.6>KTRH;?_`)<B!^BL4L-EWW4D:1NIR,F$!IP`K`;.=D[62X!2 +MJINIRA%"1*OE@R(`^JC-/U0.5!4%(@9PC\H1,[JP:8&#$%-H[#"FE5:E&1*J +MBW\RN`6\[+4-,QV3]/M0ZKJ*6$2T+.F*0&D;<!17'3F/`AH^RT]/&F?92AK6 +MMG$=E-1IR'7.B.?2.`0LSIO1/)=ZFC[+MNH5&Z(T_14&^HQH4])O9MF?P#`V +M7`>REHVC'0(5ZI:U'$P"FM[=]-TP5C*S>ED9]QTX%X@<\*:GT<.8"6+:L;37 +M4!<%K4K6F&Z>RSCA]:<H.D`-!TA2T.E0Z2V%TE:V;B!E)M(-`.5UF++,H],: +M6B1GW1/HMH-($>ZO5:S&LW.%F79J57P-N%9C(EY0/8VK4RR<JU2M*#698)04 +M:19$@R=U,`Z<@Y34MY7F#9:TA_0$YHTFMPW*"7!V)3OU&-)*:B<B&D-``@IG +M/(=AOV2;3/\`4EY3@XPKP:)SC&WW3EV,G]5(*9:R0/HJ=:I_,CCLLY9Z60US +M<EHP%';.-P^`FKEDP>>ZFZ-0&LOXE<YEEMK46J%#2,C*D<`T2?U15:M.F#D+ +M.O;IS_2QR[R,6FZA=ALAOZ+.%)U6KJ>,%7*-N7G4XRK+*``@<)645O1TMPV% +M*&CD*4@S`,)0"?=601D'Y_LJ74Z0@&/HKYDG!5?J(]`D[*I4=`:K<3!`4;J0 +MG"L=/&JC.T(W"#')6/\`4RXK.J4(<9"C-/@K1J"./E15&3D05L4_+$8RDQD- +MSCY5O2(E1$;@B$%2XHM<TM(!E>7_`(E]'_AKD75`$23,!>JOP")E8?B^PIWE +MBX.:"8*93<3>G,>![L7O2G4'.ES1&3[+D.J6O\)XK((_,XG]5T/@B@^TZI5I +M%IB?@)O&]H1U>C7:`?5O]5,,MXV,Y363H>GD?P=.,X&R>XVSA*P'_HVF!D!# +M=P`,K>*5!5V/"HW!,D#]%/6J0)!E4Z[B3+9515JCU<?=4[C\V3"N5R)!.%2J +MD$Y^Z(JU3Z\SLJ=O+KEQ@QRKEUAITR<*IT\S6=D_52]$7&P'1'V*D9J<[T[H +M-!C8%'0@;-GX0&[\OJ.R:F&X.K9.(G,DHBT$=CPHL.`XB92AW=&S3I$RG]/< +MJ;C3S.W=+)[JY;5!@'8\+-MWEM*-6KO"N6;IS/W7=P6KR"1IR%0O&X#!_4<J +MY6>($[*O0;YUPZI,M;@+-6'>P>3@Y5.L``2<=CW5\!KG$=N5!>4Y9EON$JL: +MH)J`D$CLI:!)<``(]U)=42*0=F?904FD.F00#L"D2Q)>M:6M#@"?CE'8.#'X +M&?V454&I4B<`<Y*>FZ'"00-B4B5IL+:A!&/E7>GW+J#X;^4\K,L7S5+85YD: +MM)C/Z*SAJNQZ+=M?3$>PRMNG<::)*XGICWTP"PB)6W:7IJU&42XQO"U6>G4= +M%8\36=C4M:VENY]UFV-1OE-`.!'[+1HN$3,+&FEEASE&X225%1@Y)4\0T&8! +M4L:!)!`"M6U0$Q]%3<,GD_*DMW:'=Y1&K2)B8A-#7OGLH#6TT9$!3V)+J>HQ +ME8S+4VK13+BJCGMJF0=BH?$UT;:UP=U1Z1<A[0=6_NIA.$C7H574G2<A:%"L +M*C1,3^BHT--294C&.9EI6W2+FKT[;?<J>BX0J=%X=$JS2QSNHTM!P`ALGW2/ +M>4+-.F!^J*0!D*:4%:&LU$!5*`-6L29@84EZ_P!):'&45C3AD]TJIF@2#V5# +MK-DVYI$`96BXP0(3.`TZ5,IM8X*X8^SN""#$JW:70\T21D2MOKG3V7#"=.RY +M.X94M;C27>F=UG&WJLY3['96E753W5JG4).-AV6!TBZ#FB7+9MWR)E;TDJVP +MR(/=3,@`S/95J!>7^IP(XQLK$<\J-G:)R#(1`<D)J8/;*D;DPI1$^F79!`4C +M:+0WW`4K&M&#NC;N,84TJK7;I:K-C3#:<[2HZK2]P&%:8T,I:8'T2K#B!RHJ +MSC'8J0R_`4EO1#R9E.A3;9NJD$@Y5@6+&Y(`^5ITJ(IT\H;EHTDA36U4J=$: +MM$*9UI2+?RH*!FI,X"LZ].Y4UNJK-I%CI`,*1E<@P>RL`->T84;J(>9[%76A +M(QP>-]E7ZA#:>#**K2=3RT_=4:M74_0\\[J\!J-)U0YF%890:!NI:+0U@&_P +MIV!H"FOR*OD-[;G=%Y`U1"LD-F>4_I@<IHVJFW`3MH-Q"LB"-N$FAF"#":$+ +M:`)V^4A;@[[#LK!+0))CZJ)U:FW=X31LWE\8"JW%BTNU$HZ]]39L9CW69?\` +M5M.&3E+A+V>^AW]M2;#IV04:[:=.&!0-K/N0"\DCWX4U*FP-RDQDZ2VWL%4U +M:SX)PCIV^@[`^ZL,\L#>041@[%:TR&FP!IGA.-_A.X`#TF4$#5NJ'?G(*$3` +M)^$SS,]PA>2UL]T!8&(S\JMU""S&2IM7V4-_`IRWZJQFGZ8TZ3&RDK$:@=H4 +M/2\-,E2UXF9P5SLX3()<")(]D#R`(*7[#*$YAP*W.E"8TX0.&9`GY4D3_4AB +M'^RHKUJ<CTR%4N:.JD0[E:#Q.1N%6O\`_EX5B5Q=>V%IU8O:/S'^Z#Q/0\SR +MGZ3W*UNI4`ZX#SIE4>OF&,`B9W4DUMF\E;PR@P#``4%TZ2<?JB#M5,`G<<*K +M=/B8(GE:G#*M4)DP<*"H9&)SPI'.!F#\@J"J=.SAW1%>OEWM[JM4#OS<=E8N +M#J]H*A<($`_=!3KM9I)DY53IP'GO),$<*Y<?D=..,*ITP.\UT']4O1%YF<1^ +MJD83$1`'NG#1,D'W'=.3+<84#.(<!!RG`[DIZ1:1/^J:L"#G"BI&_EW/V2^O +MZ*)E1FD22$7FL[E--;>/4*CJ;]1)<.%KVM21,P>RR[?3IT.SV4],NHF=Y^Z[ +M;_+RK=P]U)AP>PRKG3F>7:^K!(G*RJU7^)N:=,8:W)6L'?R2W[*::@0"PD\( +M+C\H$3/Z*5Y/E1$DJ/3.\[(TJ5VX`@[S*I!A95(G[K3J`D9:%0>#Y\:02HE5 +MVG_U;A)!`P%-J(.9]E#/_JR,R,;84]P"TSIGXE$Y%0<:;I#MSW6@Q[?S+*HE +MSJ@!&^P"M^8-@[E58W.GUG>6<K2Z('U+AU:?RX7.V58Y$D!;OABJ,@DS*UV5 +MU=A<NI.&HF%MV=TVHT9SNL&U#:@$1*O4&.9!;Q[K(Z*W,MW5RG.W985G=EK@ +MT\[Y6Q:56N`,R%*U*DJ@`2H9<TR)^JM/TO@JL]L'(E94=2Y'I870MBP_Y+3. +M!"YZDTOO6[P%T5`%M&0N67-9O;'\8--2E(_I_P!5B=&K%A,SV71==;JH'4N6 +M:UU*K.5O#BMV<.MZ96U$0>%J4CJ;PN;Z/<9`G*W;>HV`9)6S%8JLV<U'1JC` +M(3M=Z1"&I3CU`+/3HM4W9PI'/&GC"J4:P@ZC":K6!PWE4V6H5KF-X*O4QB`J +MW3Z`8TEW*M`9V69^6C%V0$;8(D'Z)B&XG=$&C<X0156M(((GA<YXEZ=K];1L +MNG.^57O*8>V"-\+-QVNW#V-1U"KI=N/==!TZZ!8)/LJ/6[!K'ZX&%5L;G2^. +MWNM2[<[-.JMZDG#L*SK$]UCV=QJY6C1>#C=-+*OL)+)E24CE5Z1ALD>TJ>B9 +M(SMP%&ED$$`;]T1_+,[**8*)YTL10T0'5YD8'=6JL0%7LBV22`K+&!SO]5EH +M]"G!E7K6F&[0@HTP8PK--H:`"Y%AW`!D2H;HPS^RF>0!G?Y5.\)TQ/NJE0V@ +M'F&45P8YP5%9_F),[JQ<4R1A94%"M!A7:3FGZY654)IU([*S0J>D=RM)%NZT +MZ3`&VZQ<&N8V6A7J$TX<#"S[9NJL7`*6+M:+](QPA_B-)DIBSDG'9`:8]\\J +M@_XKD9A+^+&5`:?J@=^`F92`=D%-HE_BS)]TOXP\%0BG\I"D,%-B2I7J$1)4 +M%057XU8*L-:`$X89SB%-BF+5Q,.<?NJUW:`$20M8#95.H0"`2%=<`+:V:*(+ +M2?NC%)VTQ[RI:0!I8.GX3GL2DG`K.;4:3#DPJ/8W.1"M$#<J-S`>$1"VYP9' +M$)V5Z9`(*9]$'X41M@#@P$1/K;O,<?*(D1CXW5*I2>T^DJ-[JH]U=C0])&-P +MJ]_4;Y<$A5Q=N!R%6NZFL:M7W59J[TUQ<V.#LK-5I$$\JMTA[?*)F5,^H"Z) +MPN<EJ7@)'I$3*0$"/JDXM/*0$N@25U:(0/E(029"<3&R=\#VQLH`>`#E4[PC +M3."%8JN),-5&[<6X,JLUD=3J,;5!,?*Q.MW-.I48UI$K9ZM0UTBXN`PN.>QP +MORTN)SS\J\:VQ]:A<!3EHX5.L_48$X/*G+O1`.0JCYUD"2K.D`[O,J"IVB.5 +M*^1&-U`_5L4$3AJV*C<V1!^BFB#(0P8@")4-J-TUVDDJOT9I-1T?NK=P?26@ +M1'NJG1!ING@_U83+HC1+0'$@9(SPA:UWY01'<<*<M'"B+PPGE`5*F!'8*.KD +MD/E2,=$$2HG3K+CO\J0(-;`]2?2S_$G#C'"6H^R-</%;%\.&J"9V6HUPTR1( +M&<&%B4*K34])(]IV6D:W\EK&B2XP(7:O*.U#]3ZS01G"T[.X!;H><^Z"@P,I +M!@R.4C0TN)&^ZSTWI>)!@`(7,(4/3K@ZB'JW4#33F55BM6U:2T`'4%GO]%:) +M(/Z+3=3BFXSNLRZ#6.)=R,+/TO2L[2ZL7N`[*\UC74X':!E5K>D#1U%NYY4U +M":9`F0>2D$%9HHUO?=*K^77,%3WC&N]>F>ZJD#3I,P$5;L7M-,ZCQN%L]`J^ +M6S5.YY7-V=3^2X@''"U.EUY8`>/=;B1WG2K@$`D_9;UE5#V@E<3T:YD!DKI+ +M&X``$E+$Z;HIAX)8`3Q+E:M0^F1G!5+I]68]2U*+@6C"SIK:W;W`C2XY/NGJ +MP&EP)4'D2-0W[H;ASVLT[GY6+56>F4O,?J$[K9HP*<3LJ'1O10]6>5/5KR=` +MY7/&;J3M!U-IKO#&G"Q.K4-!CLNGMK>*9>[<[++ZQ;R#CZJUUTQNG5?+JPZ5 +MT=A5+V@`Q]5R[V^74Y"U>E7&(E=)S&.JZBW,B)'RIW?EG*S[-X+9F58K50QD +M24TW*@OJ@;L<GCNFL&U''4Z8G9-1H&N\.=QW6A3IM:W"QW5UI.RHV!*G:X$S +MC"IFG)D'V1`/:%6EL&3@I`D_TJNVH08E2,K-,@N@*:5*,"80U!(E(/9,RC.6 +MX<IH9?4:(>PXRN5ZA3\FXD8GW7:5@'",K`Z[:C27#CE2\%Y4^FW):()6S:5@ +M1)*Y6D]S'D=BM;I]>0`=EISZ=-;U1Y6\PK5JZ3LLFRJ2)F%HT*D[%--RKP=* +M&L\!D3/U0-*&X+@WY45-8"3NM.U8/^ZSNEL<6:EJVH)B2/A9;6:;0.,A%(.$ +M&2-PD>$43^TRJMZ`*9(W5C;)A5;QTMP/F54J.Q:1)5MP!'!56RGZ*RXB5%5+ +MFE+I4+':7Z<J[6$@E4JC9=LB):S@:<3A06Y(>0"5)`TX(4-%T5HW,H+1[$2% +M%IWR84CL9P4SLA`,`@;X]T@V$1_+,(3G?ZJFS?LD`",<).$#!320`-,('#<3 +MRD02(U$)]1_[II_W*!<PJ?41D9/RKA^BI]0R.?H4B4=MFF,(G1)R906CAY6D +M.E&Z`Y(4Q)C!E-.=]TQP<[)$PJ$X1S^JC<Z793DF$$YRB$3G&2@JP3F/E$XB +M20<J(DF<JA&FPB("I]0H-T^DB2KK=P)D>RK=1ANG/U1**RHFG0])*"X:\OU! +M6*+HM1/*$9V"QC$O:G-5IR20=E+2KN&3LI7@")3M:TB0/NNF@S;@!LN3>=JF +M(2JT6N,!/2HM`01OJ!K3/[JE5.H%Q..ZM7H#1M"P.M]290IEK'"0K)MFW2KX +MEZ@RA;%K2"3[KF>G!U6H:A$F9RH^KW%:M5-1P,#Y1V5RT,R(E7.SB1B;[JV_ +M>0%`]I.VZD=7IEH@P9V4?FLF0X#W01OU<M]E&]LC;93RUSB%$\>J`0@A=CZJ +M-X`._P!U-4B%"1G,HBM<!L8,E5>D0+I\DY/=6KD0<D#'94NED,OS/)^ZF72X +M]M>L,X4)@G(W5BL0!$[Y5=\[?W1",QJ#20.?91U#J=+?DIR^&1_=1%X@D['& +MZ*E#V`;):V=D#64M(EZ6BE_C4TKP6RJ.=4`)F.ZU^D5/-O9T@Z%SMN[RP3R- +MY6OX5KZ*+]0`<YR]&GFRFG3T@-4G)]BIM&KLJ=M5)`/!PKE/5/=JPW+#OHX] +M,`^R!M6HUVEPPIJ4M.43J9>W.Y4_XI$BJP1/NL[J@T-/I$*U4:^D^!.RI]4J +M"H!3$R=PE4]L!_#"#S*CR:F&F%.&^4P-X]E6I:C7<9(XD8E/J?%QH#J<1,*G +M<MBK$1*NT7'`.GZ*.[I"-8`^B56>WT%X!C'!16-=S>25#<G0YSCL@MW%M/;= +M6,V.IZ+<D$23A=/8W&IH/*X7IMQI#9'RNBZ==MU-,_"T.WZ94=`),!;E"H"T +M&5R72KEKFC,_5=%8U`6`YCY4L)6U;/DY".YA[QA5;1QU29A6:)UU`)GY7+.< +M-;7A4;2MY<(1=+:*S]9.)5+J1)#6"<J_T2`P-DPL83:XS35T@-C@+.ZG3U`X +M6HT`MB56Z@P%NTK5CK'(]0H^H]@JMI5\NL!)W6MU.D=1QA9%>F6U)3'AG*.D +MZ;=`L!R/E60\UZX:#(Y7-VUWHI[Y6_T&IJ9YA,F5K),:VK9C6TX&X4A@;**@ +MZ1DB%)CV'PLNAJ3C)P5-3,NS*!C94C(X&%%%H8[?"B=0G,*8D:1.Z)H&TQ*: +M7:J*;QWA-JJ,<1RK;?S1PG<QA&=PIH4W5C'K"H]3+'4CW*U*M%GL5G=4HC1$ +MH5S%W2TN+FF`FMZL8E7[RV=Y9F)6/6.A\'ORF-8KHNGW'I&5L658%H]4+D[& +MKH`DX6[85P<`[JTE;U%TYD(;EY`B5#;O$?W37+QJ:)Y4^--KI##HR=^Q6G3` +M')5'I&D41C)5X`D<J-Q)_3";.F9"'5"<ND(I.(D2Y5+XB-U8)Y@*C>D:A@JI +M4]CLK#`W5E5[/##*DU2[!A9X5)5$[*K69ZIX4SW$Y`RA<)YA+5T@J,=C2J-= +MYI5I<M72"V8(*R>M4\2-PB5<H50]LSA&X[$+(LJ^1.X6BUX>T=U;$E2@F#E- +MQD)-&.4Q'ND4YC>4+R3]$VXG8)I;R@/)$(9XA,#`W3:H!$H@I/('LJO4/R8, +M'MW5C5[_`'5;J1:&AP.?96)36GY.WPI`9)4-HX&EN4^J#`<2D*)YEVZ1SNY, +M82;AI&Z!W;;H,3_FDXD&>4+B1F50G$`PA<(.^2FY]3D-0R\"9A$.YPVC/RJ- +M\[4\-/W5RI@;Y6?=NU5P-XY3XB]3;_(!S]TB0#A$`?X<0<J!KH!!68E[/5?& +MQ4;J^@`RFKNG?A5:ID:9D+H-%M]0T22)5.[ZU;T\%P^BR[NB]WY7D*A<=-UY +M=4/W3<GQ-6_4W6>O"H=%&2?99++>K<O-2N70KK+"E2=,!Q]U,6^F-*>UR_Q- +M2,'J]*FQS6!H3LLZ;J8AH4_5VS6;D8*F9I%,-VE+VDZ9=Q9-.Q`52M9N:8:Y +MRVJNDC;E0N;JR)51CNIUV<DJ!U2LTD03[K:J#!("KOIM<"8V09;[N!F1"8W3 +M8,'"MW%!E1TD"57?9-),&$Z0-6HQP`+N.ZS*3VLZFP^_=6[FSJ`#2Y8M_3K4 +MKYICG<)>J8]NJK$%N^%"20W.RH-K5O*!T\*$WE0$M+<!2:*O/=+M(43H&)@# +MW55E^#,H:]XS8X[JBTTG3N$\GN/NJU.O3+`0=T_G4^ZC3YZ-8L!#P<X"W>AN +M:VBWM/)7-5W/\QI+0X.,#NMZPK^50:<^K@B/U7HG+AG/PZ?I]1KR`=S^BU6' +M`$GZ+!Z56@@XSB5LVQ+@-UFLXK31G)4OJT@`?51")D'Z*3U&F`0LNAVL:\$N +M"RKFBX7\TY<UJUFN#:3B<X5>PHE]$U';.*E(I/K-=Z9D]U&_\Y!,8[J2[H$5 +M"]@)X@*J'Q(,[[#9)R7<7;9N"=D;Y<PP@M]1$X@<*RP?R)0C#OV`%X))4-`@ +MTSDB%H=0IP"3&!A9MJ^:1)`P4B5<MWF=1,1PM6TN-+!!E8]"HUP,">P*FM*L +M-`)6H1W71+@%C3JRNGZ;7D`2=UP'1K@!@`<NKZ;<-('JQW*U4=C0K&-P(5_I +M7J.KLN>HUVB@!,:N5T/1V_\`I@[NN&<^&]KE=@(/[(NG.#'@2F+01NHZ/HJ@ +MY2<.K?H'4T$.2O6C3)W473ZC74\%3U1(D\)6XPNJ4SJE8E^P]ETO4J3BQ8=[ +M1,%PS"R5B7#GC=Q71^':Y\@#5^JYWJ3"!($?*T.AU7LIB7+<Y8Z=E;U3H`@* +MQ3?Q*R+*L74A!_57Z#I$R5+&Y5T.,D`HV.D>RAIOYVA22`#.RC21CI.1A&3] +ME#(#2`G:??;A%2B)WD]T61B?LHVD;''PI0#&%E35`",F0LZ_C6&@Y6C4.EA) +M,?"S6M-6ZB9'=+T`KT/Y.DB5S/6;?2^=.W9=A480-IGE8O6:1F8GW4TE<W0J +M:7Z286[TNJ"X96'>TRRIQ)W*L=)N0QX!=.5J<L7AV=J[4P#9$2'7+6JIT^XI +MEDEPD[*>B[5=MC:4:CJNG@>0(_16@Z/8?*K63HH`@B5.YT`3"SIT@Y!'"37" +M(.Z`NGLG<X"(0.3E9U[.H9(SPKU4R"!B%FW%2:H$B42KML?Y??W4X@"5!0D, +M$%3[A2-!)$@`;)';;=(X$PD".<JAGF/3$K-ZN06F>RTA#L0L[K#0)D<=TB5E +MT029&ZLV]32?4HNG`Y!;';*>NW2[5)3%*T:;Q(`.Z-L]BL^UK1)<-E=H5`6^ +MGE4V+3B?T0.W_P`U(_NHW$``P44B0-DP,`2GYG2?E"3/QV*B"+LQCZ*MU*#3 +M[J5[VXB(4%[4!I20K"HK7\ASGV1"=?S[J*T=Z20$;3)F$A4T3W*8$SC]4['" +M"!PFD?EC!50GNRHW'NB>8X4;CE`_]/NHR9=(Y12W3N<(,.$Q]4`UC)A4:\&X +M:-2NDR8./94;@`W(R$O2?6I3]5'_`#59PTO+295B@[TCD?917C3(.RG255KG +M.ZK//T4UP02JM4B,GZ+:!JNE0U?R[?52$MX*BJ?EW05G_G3.YX1N#2<2FTG3 +MC[(C$ZP'><V))]U.S\C0[]$'6F36:[B<J4-'EB"E[2=(JS0.,%0_E=$?JK-1 +MH.)^JKU&@'4(A5$-4G6.Q05(R/V4E9DG&%$6DSW')05:V'C,IO3!AN2BKM^Q +M0-CR\?4HB"N1.DC[+'ZV--9KNQ6PYLS[;+*ZX/3.K961%NW>U]%IC"CN:3)P +M(!3=+<'6HD9/NBJ%PQJ6<>BH/(I%OY<_94[NU8X%TP>5>&3O!5>Z$-R95TLJ +MJRU>&@-=A/\`PU3NK--P#``#]T^O_I6--OFRL\%U,F"),9D+2MJWE46%L25D +M6]1QN00=4;85ZFXEY$@C<$8XRO5'/*.IZ74U071@\%=%T^JT;F5R/2JCF@`. +M#F]PNAZ;6AVDY@;I7GG%;K2"V8B=BI'.@1S\JO0<2R(V"L`$MC$!8KK*K=2> +M!1#`8<]7+-I9;@"1`*HN:;B_V]-,85^K+*9C'UW655(EQU?94.I4"'!S)$[K +M3:T:9,2H7L\QV<II5.QKC4&ND'NKQ+0V=7YN52J4OYA#9!.93N<^G3:UYD=E +M>V9P+J30ZF0YQ(CE8K612).-6(6W6+:EE+I$X*K.MVFF(:#"EFE[9MNXZH8V +M#/)4VKR\.,_W356.%:`2)454D3[;95EVS9IL=)NF@`.)$''NNIZ1=%S!GE<! +M85'-J`N,2<KI>F78:WTSGB5M([SIU<5+RE3F<_"[OIL-MPWO[KS+P54-QU$' +MM&?NO2*#]+!G"XY<Y).UZFZ'$<_*&I$DSLH&U1JW4Q=J:3W2QVE6^E5G!Y#X +M@+8IN!:N>MW0_!P%J6U:&Y,(W!7K)D1A9%]2(:0MUYUTYD!9G4&3(V^%FM.8 +MZC3_`)>)^JALJA#8$"%I=3H^@[8652@2!NK&*W^F7&T\\%;%G5U-B8"Y2WJN +MIQN?<K8Z9<2(U#/NJDX;['[05,P@O$N,K/IOQCE6J+YRI8Z1;VA.P'^K*C![ +MYA3!T#&5+%@Y(_+*-I.2H@1DDR4GU-(.>%-+LUV^!'*"S9$N)RH*8\^MC(5Y +MF`!P%+VLZ"Z<D<+/OJ)()B3[K1=(!Q\*L]FMI/"5'*]6H@2<>ZRV-#:@>.%T +MW6;?4TB..%SMRPM?&PY4C-;_`$DEU$96G8!PO!/"Y[H%<L<&D@Y6[0K`74RM +M:25V5J0&C/ZJ9SAI$K)MKN&`<*2I>0T;J.D:0?'^92U0-RL]MWJ$DS\)VW8` +M[&47:]4?Z2>2%EUG?SQ.<J5]T`WU$?(6;4NVFY'J/Q*7I/K?H.EC<1W"E8X$ +M0-EG6UTT4]U-_%-VF%--;6R1`$?=)A$;[<*I_$9P04[:XTZIB.%=&UI[L@RJ +M'5WRTP),*=UPSO)^50ZC4#FELB$D2U7LWB8Y*FJ-!!)&5#T\DY@*9S3D!T<J +M155P(,S,%6[.K``,8455ON3[J%C]#@"K$:I,B1N@.(YRHJ521DJ02=R%*'J. +MQ(,A1N,B>41`!.<>Z!T"2,2@!Y'!*@O'>C'&V5,\S_HH+LS2.\K42H+)X(R= +MU8:9)AQSQPJ=G!,?NK3(F`T!2%2?TX3SP<2HW.(P-D[7%Q"H*3![*.I,P70B +M).GM"#5ZT"QHW2,`1.R=WY=Q@(()!,R50!B2`250N#INACGNK9_,8(RJ-VXF +MZ"EZ3ZTZ>6M]MD;B'TR"H6$!@/9%4=Z-RFMI5*X&DQ./=5JK3J@*W7`)SQNJ +MS\XP4B17((.(0UL#<%2EHE1U&@B()(5$%)L@DC)3$>HC92AIG/V2+1N$1B]; +M!!F$5*'4`8"DZXTFB2,$#=5.F/#K>!CV5O;,$]PU>W)4=1@=&DX2=.J"(^$X +M<-,;=E16KC)!W"AJ/AT$*6X/K.256J&0<Y50-8`D\*%H(YW4DRW<X4=9TNW@ +MC]5!#7(GB2L_K-+5;DM(VW5]SM39=NJUZ"ZV(/8K4[9O2ET5Y-H<G!4U:3_F +MJG0W0]["=NY5JMB=2QCQPN78=+-BH+@SS@IV$Z\R@NL'2)GNK2!&D!/+5$TC +M2,IY'=&]OF*PN'&H(VQ!E:UM5)=!B3@`KF[)W\SU8C8+8HU&G3!D`X*ZXW:9 +MQO\`37D/'OL/==)TZJ&!LC)W7*].<'56`20WE=!2>W0'$B=ENO-DZFTJ-=3W +M`]E9?5TT"XC99O3Z@-,,]E)6>^K=T[<'$R5BS2XU>Z73_D:_5+E9KSH`.=\I +M42&-T[`;IJH:XQE9TZ2A8!`!(PHJI]4@XYA2M`CDJ!Q]L<IH05/34QWR"BO@ +MVI0@-SNG?3#G2/U4=8PTMDQ""@ZMH:V@Z9)RKH>S6T2J-)@KWT$0&^ZFNFFE +M6D"8@)WVR*]IM\T;*G=LTLD.$*[2+'TSJ'JW454!U,YSP5-::WMF$`D:'1W* +MT^GW/I`:<=E1=1)!(`QRH:%7RFNP1GG8K4K%>G_A:\N]7_5]MUZ'YS=($PO+ +M?PGN`ZDYWO/[KNZEP=8.K[K&N7/&\MNA5EX$A7J#@6[_`%7/V-?.#]5KV521 +M\K5CO*N#TO!!*TK3348%F_F9A6.G57!\3CW6-?&]KSWN83,PH*SFU,X'=7G- +M#V9"I7=+2/3LLUME=69%-T9`"Q6LF1L#WX6QU1Y%-P&)"SZ--II&3]D1")D@ +M&85JPK>7'=0-I%K^W*3AI=^:0KM'16MQJ8,G=7Z%3T8."N8L:Y;`U+9MJXC! +MW5TLK8I/)&\_*F:Z,`X6?0J3_4K-*H8_-NHU*M:]B0J]W6]6D<IZM6&2J=`^ +M;6F<=E+=*T;*GIIB-^RL,&J)_=04G`"(RIFGTRI(HG-Q"C+8$QNCU"-TU0X` +M[;(*%]2;I(.,+G.I6VDN&.ZZ>Y!..5D=3I&/RY42L2U(I58SA;W07BO<YSE8 +M56F6O[+:\&MFM(Y.ZK.G94:#-&R8T6]P5)1EH&?HD3G*CH`4&`P$G6LGF=U* +M#&V2B=4,Q":%6M;PTP1`6;Y`_BP,F5LW+H9DY6?2<UUP$LX/JTRW@0W"-EL0 +M/4284K'8V1M,M@<*:Y57-)W'*847F<JV8W'=.".0J,^I1J#+3$*CU`O:"'2M +MUQ]N%D]9@`DI$JK852&",JU_$M<<E5;-@T?*G--KR3D$I-B5M1N<X05=.X,R +MHGL<T8G"!^L8""S2?I<`7`JVUP,$+*%2,D*S:UQB"G8OG2=RHJF"8,@H@X$" +M#&-BHGX.#A`G$D08^JK7?Y#LIPX1NH:PP03NJ52LGCS")&.95L$`$%4*9T79 +M$B.ZMN</E(B0$3(.W=$"<F84(<)PI`09<1A4$T]T!,&?V1C#2-X4-4[CA0$7 +M%PPEO.$&J(A.W:`51'5Q*S;@D7329Q[+0J&'8_19UX?YX,0EZ1I4ZD,!A#<5 +M`QFZ&D\>4!^JBNAKIQRD2HVU=>=_JF/'<(;1GELR[/[J0C,I.C2*K@J,>H'( +M"DJ\XCV40D"`(E4"X0XC.?JFJMC:8'=%$$H71O\`NB,_J@_E.!Y"Q^FU-+W- +M@A;G4&!U,SD$+G+8Z.H$#<[JWIF=K]1LF<D]]U&\;QA3U`-*KN=N-R@@K@@C +M]E5?CZJW7,;'(5-T$Y)PJ@7G8C`*"KR!(CNCJNTM`DPH7DO$DR0H(*IGC/=` +M^#3/J@_*58;EI48.X=&1N%48E(MI=6+7.Y&/HM&JX3!V*S>I?R^JM>"`!RKE +M9XJ-#@GVK>9"<8.3,(:KMYW"![W:#&Z!SG%GJ;!0@J8]`X3Q[H&5'!H&H?9/ +MYKO\0^RSPV^3`XT[@M(TP>2M*RJ!SA)&>52\04G6_57B``?=*TJG!C=:\>6X +MZ>3%U73GAQ$'!,;K>L7:G""<87*=%KM:R9]<X706=RXU?-<07'?W7H[>/.<N +MJZ97#1)XY6ET?34J/N"!DP(7,6%V_4*;1+G&,+JK!OE6[&`#(6,HQCVOM,D. +MF$1F8*C;M,_1$VH($@D^RRZGJ8;@084+6@B2#[*2HYNF-C[J-KB[`B`H'8T! +MWPHZU/T&<%2T"T3(_1-7V)SLFC;$M2?^(OC$=E=<[U$.$CNH+>F75GU=,294 +MCVN!![I.D#4ID-<YFT*NRH1NZ1[X6@W%#D^RJW5`-9J`(^BBJ]5H<):)![*I +M>TM%)SHB1"TK<@M@`PFZC3#K5Y9G'"6Z339_"2MIH.&6GG]5VG\9J>?5/NN` +M_#:6TCP5T3KDMNBV2IC=UQDYKJ>G7/JP2NCZ?5!I#,%<%TR['FAL[KK.DW&H +M`+I8Z8UTMLZ`#RI@[34!&%1M:LQE7(!9@K%=8V;)X?2&4=5@.ZS>FU8=!,1P +M5HNJ!M,PX+-;E87B"C+]`&5GFDZDP!TB.RV:X%>[WD!1WULTL,#(V)6-ZJZ8 +MU,MU[D^Y0W+9.,#N%+6I.IU%'4>-B<JHKTWEKA'ZK1LZX']1*RW`%^(^5/1> +M0))B.RJ.AMJH+!ZE>H50(Y7.V=?.ZTZ%Q#-6T(LJ]>5Q.D&"5/T]NFG/<K)M +MZHK7.^!RM>DX!D8@<RLWMJ+32`8V4S2(!!WX55CP#ONIJ3\YR/=54[7@C)RF +MJ$`'8!#,F>>R3AJ.5`#H<W&96??4YY6D_`@!4[EH<TD[I1@WM(!T*[X/?IKG +MY0W;!!D!'X7HD5R1W4G:.N95$9V(3FJ"0(5=H=M*(`$JM18UB,%(U"23(5=L +MZ=TVH\<()+I\M(F`%0MW.-S@QE2W+SIG95+*3=:IE+T3MNL=#1'*?S.)5=A@ +M!$)F2@L:\&$XJ'`4'P$XE*HR\3@DA9G6'2KQ$YF%F=7B8!1*:P<?+A3:HB8P +MJMD3HC]U*7>HSL/=2+5B01\I.`)[J)K]N$X=ZXVE4#4I`@Z5&T%IV*L.<2-T +M'YO=30=M=^,?*=U8Z<@X2`;&=RC+&'$A!`;G,1]E&^X&))RI32;MA15*(&QE +M7E%&YJQ<"(A6VO#VZEG=29IJ!P,0C8][:8$E/J-!CNY4S"2/[=UF,N8)#E:H +MUPX`2J+<^D]U#5._!"<5!W4=4C:440=.Z?4)WV46J!$I!W(1!/P(&RS+V/-! +M.RT:A=!!/T6=U&=8DCX3XE7J('E`C8>Z"H01&?NAMG?R(0ETG*3I:>(9)2ID +M%N9"$N@1O\IFD:>RJ(ZL%Q`.$+\#L$3_`,RB>X]P40HR$U68PG),25$Z9,_< +M((+PM\LCV7,51IZD-]UTET'%IPN:ZK++L.`"U>F?K5>6Z!C?NJ=4EM0S'PIJ +M;II!P,JM>._F$@_5(@*I,F1NJQ$O#I/NCJ.)`,('.,1C*(:H6G$?JJ\Z26G8 +MJ6KC(,_"KU<9B?JFBH*NVD?=0L=O.$=PXD_YJN7DNW,!5&5UP@W#3SW5FG/E +M`\_NJ?6S-P#LK5)W\EI:F7]B='.7GE)SF^7`@>RCJ.=(+HREK+A)<-ONI5A` +MXPV0GD_X/T4)J'M/NF\P_P"%&]/G7\3K1U#J!JMD-/8KG+*J69:8E>A_B=:" +MI0D4B33;F=I7FK*FG^61)#MYX7+"ZNG:?RQE;G3*HD#_`&%T?3JQ+6F3(W7) +M=+K%WHG![E;_`$JX`IZ&G;D+UX7<T\_DCH>CU75>J-$X;P%VG3G.=I+CMC=< +M'X8=_P"L<^",KL^F58TD#;NK7GO%;1(:R2!E%2;B8^O"AI.+QJ+I4K"XG$Y7 +M-T!4R3RFEL<[(JC2#O\`W4%0%KMP<HM6;4&#,`>R#J)T6QAVZ>D\Z(U3*@O' +M:[BFP3[I4/2`;;-$"2)4-;43'?@*^YLB(&.RK5&Q6$;J=*B$M#02E=-'EF#& +M%-4_F;B"%7N\-W)0X-94&NIG.4UV"V@\$3(X*DZ406D@G>([*W<40^V?,S&R +MF71`^!7<D`;A7.O56T[X`'/RH/"-'RG2YI'9+Q93+*WF2N6-U8Y2?RJSTZZA +M[7$C_-=?T"]&D9_5>:V]R0X=@NC\/7\.:"9GZ+TSE7IW3J^K25L4'@LCNN3Z +M%<A])OJ"Z"SJC2#*QDWC6BTEKA&5.ZX.C1R52:^6$S$)K5X?=^HX!6-ND:ME +M3:*>HSGNGN&G5Q$*6E&@:3'U2J@&9(E8UIT95U1#G$EJS+RA!."MVJP'_54+ +MQD-/)[*#`?-.MA&UVK\RN5K=KP21!Y5&LQS3`.`DK.DU-^G'NK#[K13^5G:_ +M?9-=51Y>J<^RW*E;O0JFHR5N,J;`F?A<GT"Y&@`GZRMVSKAS?[J:7;6ID!H, +M$*Q3+0!*SJ3Q$R?E6:-2>?HHUM<8[U8^%)J(,JNUX.?V4DPV>Z*)[OJH*Y!, +M']$;G=B@<&D&5!GWP]!PI?"P)J?7DJ'J`ACC)]D?AA^DR2D1T8P,Y'=$"-U& +MRH"S@2A+@2(<C25[@,0@),GU0A<<ZB90%T$R=_=`-T_^61./=5.GNFYW$*2] +M=_+F?N55Z:[^>2,Y5J3MNM.-T8=`_P!55UX&8]DFOG))459+S.\A)K^2?HH" +M9P$.HPBK?F-R!CNLKJCLF"85DU#!S/LLSJ50_E,2JB2V>`TD291.?F05!;._ +MD[_5$TB?41)YV4@LM>0-YY1!\G=5R[:<)P[.=D$X?G=&Q\#C*KL>"-]D[70X +M9^J;%EQ.-DB\]U"7\&8*<.X!_54$YY&9E`7DNVW35'@;G*&0[Z>ZNT5.HG.4 +MJ8'D[8_9-U+4!N82M_\`D`[J7L+RVDX*3:3ID(FN]4(VNAW"I`/UM[X4=2X< +MP9G/Z*V#+5%5I-.[=]U!"VY$_P":E;<-)`U0H*M$<*$TG;#A!H>:TM]+I5/J +M)`$G"B:VJUQSA0=2-9M/97:5I6;@:`)=$(7D`K/Z;<5?X>'`X[(GW1+O4DUH +MJ\'`P#PA<3&VRJLNFZO[(S781C'=42U'2V3NHAOLA\YFG\WU0"H)W.564Q/I +MRHJFQC"=U1L;J-[@6SJPFC:O=.,?(6!UO)+APMN]<TM*Q^K4]5J\YF%J,4/3 +MWZK82?LFN8`R0JW17_RBPG93W#?5CA9QZ6J[I`)[*%SR02IZ^&D[#LJCC)C[ +MK2#+@84%PYLD#[J7&DE4[N0=0VY0VCJ/F<3\JJ]Y#_S`2I7N)$@J"L?0851G +M==)!#I,*2S?JM!_4>ZK=7ES/92=/(;:-!S\E3+N+.DI+6M)(CA"8%,$'"5:" +MTXSPAI@^5I!!]U%@FM!;)A/H'M]T+#Z1A%/LHV\H\54&UZ%9KID-/RO)NK4' +M4.I50T`M!R9VRO;.I4P]CG')CO@+R?Q[9NI]0>Z<3,1[KGGQE*O@O<9EKEH+ +M0=.X^%K=-K@4M((_NL*SJ/IO:`>^3PM2E6UR74Q(WC"[X7ZN4VZGPW6+*+<Y +M)[KLNF5I#(F8_J*\]Z3<:'TFC`,;KL^A5?,8S(GWX7;6X\?DFJZJU?-,"02K +M=-SM,`"/=9=C5@ADM='97J+IP=6T^RYV+C4U0GYA5ZF7C?\`S1N>[41OV3&2 +M3P2HT3JFEA."!N`HNG@5*KJAG?"#J-0TK8GDX1],+V6K=49[IVB_JVF=E%4$ +MG494EN\$?/":K),D;84L:B*W'J)WE07;7:S&/W5JF"!,;SRJU1NMSL\[H([3 +M4PYG)VA7GUO_`$;@,8YG"JZ((+)!XRGZA4<VQ?\`:5/B-3PPWT!\R2=T_C$. +MQ!/J$84_A%O_`/3Z;].$WB!HJ5MH"XR<L8=N-+G,K$;*]TF]\NJV<0=Y4O5+ +M3^67,&0=UDO+J+@3E=\,ERCU#POU!II,SE=ATZNU[&Y7D/A;J1#V`E>A]!O= +M=-I!6\HDKJC6#:;CJW]U'9U7&[!`QW6>^Y\RJVDV<K6LJ>B@)B>5RRNN(ZX_ +MEMVM<&G'>%8W&3/NLBUJZ7`$C_):-!VHCU+-CI*-S1I,`RJ=VQI?[_"O'/[* +M"HT203)E8L:9=>FT$B"J=>CC;'*U+ML$@`RJU>GB2D1D7%N#)!R?T5&^8YM* +M"86U780TJA?M#J#I'"L2JG3*^FGI!S^ZV>G78TCDA8%M3(!(5BA6-+>!E:E9 +MKK+6X!;DQ[*];U/2,Q*YCI]WL)_5:MI<:B#@_5+&I6W3J>H0<?*G;4`],E9M +MM5'W4X?MWA9K6UPNU")W2<!I&?HH`_;(E23C915+JIBBXCA%X<8#!2ZEFFX1 +MB,INA/%,B-I4G:5MP(&3[HVPJXJ-/RB#C,3*TJ8]^/E!4,'>2A)!Q@%#4)V. +M%-"O>F1OLJ_3?^;GNBOG>@J'I9]9DR9PK?B1L:A&_P`)M>(*B:XS&(A,>ZBI +MVU1.Z(O:3,CO"JD'![^Z3@[28)E-B<OW,8Y63U:L-?I/*MR[;?V65U)SM>!R +MK\%RS>?+!"F+O>51M"?*!,A2TZDNW*S%6'.)_IPEN,&"HW.!.1A#J'?'""P- +M38!VB<J2F1_LJOYFWV3M.,F/JD-K#G.(QPD''<E1%_I@<)PZ!))5B)9D&4PD +M'>9]U%YF-T@Z>51%U%WH*&S(\D9^B;J+OY1SCY4=B[^5OE*BP9!D)&0Z5&YW +M\R9^B?428)Y06&ET`IY)89CWRHV.QI"9S^$#5=\)##8[H7G.#$IJ;G28*"5D +M$[1]5%?-#J9D3[2B:XYXY0ULTR"58*E@QLN`:BK46.>3&>RCM'1=.!.)4]0R +M3"D*K/H>J02F=0<6G=3EQ`"?4-)Y*ND4'4G`8)4(-459E7RX:C.Q*!S0'3CZ +MHBD]]4&0#!*%U:H&Q!5\M#C^7Y05J8#<-'U5FRZ8M]<U!@@A4KJX+Z#A)V6Q +M=TV$F0,]@J-S;,-)PTC/NM[K%8'3KK1=/;)WV)5\W;2(.ZSS;:>J1."5:K63 +MB-33"F]6GP5Q7'ED[PJ8JM+Y!W2K6]700"9A9U5M:F2-/RJC4+\&#]%!<P6D +M@*@VXK,;$%*I?#\IY5@-[@UWJ^%#7<TCG&RB=<MU;IA<,=Z79^$T*76@TT-1 +M'R5'TFI_Z8MG"GZH6NMW1)"K=$$-<X1"F7PGU:.XSNG)]#C(PGU-=(+=D#IX +M&#PI5AFL)$A/Y9]TFS&/W3Y_V5G<=7!7+8HF1@B=]EY_^(MJUU.H]HR.>Y7H +M5=S2")XW7,^*K8U*%0;R)SPKY)N.'COKE'E#7Z*A:!B<%:%FX/8T["9A4^JT +MO(O:E,-(@]]U-TYWY6C`:5,+N/7G&S1=H\J3G<05U7ARZ#FX<&D#OGA<E0@, +M9]EK]*<:=1LM(;NO3A?CS>3'<>@]-JM--H[[S\+3M*Y+1,&-ES72;IKVL`)' +MN5MT*LZ<[*6:>>72^QS#4DD[X4FN2!G[JJTOB8GLI2X8<=Q@SA8TZ2JW5'M? +M=MI-<7'!(4U`C0&;`=E7HM-;J#JN(;C]58JC0>T_HH)Z=8!P`/U[JVW2ZGD; +M+,HN+KAH)P5ITX-(-[CE6K#`'1@G_)02!4(GE6)`:0)PJE4^H_=1=I6YR3[R +MHNJ`&R?C=34W32$MVPJ?6ZFBV&PEP^J6);PZ3PSZ.D4LQ#0._P!$_6&R0^9` +MX1=!+?\`@E$[2T)^I#52U8@'"XSMQPO+.K-8^CI`XA<_URSECM.^\+HJ;'`C +MW5>_HM-%T@+=_+O'+=(N'6U=K3JP<P5WWAOJ8\IIU;#NO/\`K5%UO<:Q@:E< +MZ/U(TF-!='$CNNV%]HYY<5['X;J.KU14)!$KIJ-3,`_JN#\"WH-FPATF%UMK +M5!`,Y.5BQO&_&I)#ICZA7;&K.'$`K+IO);$J:D_R^=NRPZ;;FK`G>$P@YG*J +M6U<.&2(4X>"!$9]UG3<H*S=\!5JS);&`5<J9!RH7,$&=PLJSJ[0`=4+-ZCB@ +M[$>ZV;FFPZ@=BLKJ[!Y+@WZ*Q*SK.F8U`XF)1U*32XQ"EZ?2/D`]T=>G#HXX +M1&<7NI'\V!Q"N6=X6ELF9S!45:D2Z`,E5JE/2[&",K4J5U%E=A])L'?]%HV] +M66@3NN+M+MU&J`[8+>Z;?->&RX!+$E=`Q_$J859:/?A9M*J'`:73/96:3IS. +MRPZ'OS-%P!E'TADT_A17#AY)4W29%(P0I!?;@'V1-=GE1S]$3=.)("JI(,S^ +MZ%QW#M]DY+8WW4;S`B1\H*G47``CE1],`UDCOPBZ@X!AR%%TQ_/<\*4:E-V) +MTHO4<[*)CL"%(TXGNFC9.<`(Q/=$'PTB5&Z9G&$%28[JB34P-=.>ZR>JN:*G +M;*OU'::1XX63U1\U0`('LH59MG#R0)&`DYWJWP5#1GRP,YY3N,.P3ME)2IQ4 +M'!^B37^M0%VT)B\@S@#A!:+AW3L><*&0<1N$F'2X23'RE%L.YF>Z;4`2?[JN +M7^DQ*(/P)@H)=?`!^4=-X+<[RH`Y)K@,JANHP*9SB%'8QY>#LFZ@_P#DD$'Y +M4=@[^6=]E:D67:B<NWPDTG5_=1ZCPGU'G=!8:Z`F+LG"A:21O"1=F.$$CC.[ +MH3<^ZCJ.Q]$U-V!`03TW2<(JQ;H,Y(P%`UWJE&[;O[*Q%`NB]B8DJVXR/251 +MNSINFD#G[JW3/\H.D!3Z?#N,L@1"83VR$!C48*1/JW6@-3#MDP,;Y0UIR2F9 +M')W[HB1Q`9@E0UZA((RC<<?N57>9G,951#6$_P!U4<!K<2..%:J`Y!.%#5RZ +M08[JHP.JM\J^%08RKU-VN@UT3(W4/B.G_*\S<A-85&NLFP<A6][2='N)`('* +MS;EDF3WV6E7/HQ]U0NYTG)A5*J&DPDM(F2H:UI3,X&5*\Z3@F9Y3N(?3+MHP +MIH95U8AQ);QV51]K5:202#)@K8>88=,QRJ]1WJQVV5T;8M\VNRA4D$X5'I=W +M4;4<#N"8"Z&[:W09SC*Q:%)C>H/$R'$J7>EG:9MV7&2WY4E&Z:1$[)A;L\QQ +M@9PDVT&J1O\`NFUFEFFYI9.I%+?\85=ML^-T_P##/[K&W3U>?U'.IO`/T69> +MO\T/#I@<+7T4WTGM>\-<!(QNLKJ5!\%P@#OV73_*\TT\X\;T!2Z@^IOJS[[K +M*LVP!B#J6_XX:Y];.=(R>%S=*J]M00/RY/NN&/%T]TN\8WK<^EI<TNB%K6+V +MN+3!(`[K"LJKG-`.0=BM;I]0Z@20(A>G&N&3I>FO+;A@:YP&"(."NBMKJ:@; +M+=+<8*YKHU:=,P=(6E;^:UX>X`P9PMWAYLG46Q,B<M.1[)7]8,ID1I.V%1L; +M@AK9,\917-8UKAK6F8=)@%8L)?BYTR6TA+8<[)5@@NR<SQV5:A5DP9AN%8ID +MEATX6=-2HJ;=-WJ/!6C1,CZ+.>?YD.:<G>5=M7%D#@H;'6UBF2=O90522PQO +M&P5BK4EL$*O6)R-(AR+L]&12C:5E^(7PRF`[=X!$;Y6C.BC)9!]C,K)O#YE7 +M46R`X1/=3+IFV.SZ"7'H]$$02!CA7*P+J7T5;H'_`.44]P(5UP::6VZY3MSP +MJC5;#Q,#B945>D#D;=E<(!(&)35F12F1]%T=]N8Z[9BI,M&W*Y:Z;4MK@B3H +M:X;KT"ZH!U-Q+<KENOV;2UX#2!*DOK5LE=/X#ZF&T:;=63&)7HG2[EKZ33(S +M[KPSP[U#^'NPR'-CNO4_"M^*MNPZAMRNV4W'*<73MK>J(`!D[X4XJ$C$A9-K +M7Q\8PKC*DLP9D+C765?H7!:[2=CRK]O5&D#NL4/`$SE6K.YAP`4:VU]6(E-N +M#PH*+PX27$`YW4@>"["S6Y456F223A9G5VC0["U:SN%E]0<'5`SDE016E/3; +M"!&.Z&H-1DC?=6PS32;.T0HG!H<?\TA5"HT:H`V]U7K,W)P>RT*K?48C?ZJ* +MM3EL@_,J[33*?3.K41"*G6=3J`M5FK3(SLJSQ`WRK*EC9Z;?D:6..8W6S;7; +M2!#IGW7&4ZFDB)'LK]M=EH&8@)4G#I;JN#2)D#ZJWTJN/(&09]UR]3J`>UK- +M6^`MSI&;=N8E2\++MK^=VVV1LK>KC*H.<X")QV0FJ01+I4;:1JC$;]D+JA._ +M[J@;@-<&NJ-DG`E$:GJW'S*`>IU/1NAZ62*8,[E5NIU"6&"$73'N=2:`=^4J +M1K4ZO!/ZHV5H'YC$JD"<P90&JX$RD%\5B[,H75M+M_NJ3*CBZ)PE5<X@Y&%5 +M7*E8%D;K'ZC7!J1/*F=4=P5FWM7^<)G!V4O2?6I0J#R1C]4G%N_'*K47#R@0 +M>)"/6",G=1I*7[ANZ1=&5`7PW!A(O^I518;5)[(M6TE56N``(")M01ML@N!P +M@0=TXRV9"K!\B$8?!03AS28!D]D;2.3]%5UQSGV1!Y@[_P":H74'?RS$=H45 +MBXMIP"`=U'?/EARFLG2W$B$OPBTY\&-^4P=F4$CG?N@U0X[0B++7$'Y2>[B5 +M`U_<)R\[$G"HDJ.(;,IZ;QIB,PH''N8RE3)F)*@G#R3'',*<&6B`53U28W^B +ME;4<&C*HK=5!:=4;*6SJAUL#]"HNI^JD>?[*'IK_`$P3LE2+-5^<?,RF:<@G +M=!4(U'GV3-B-U055Q+C!PA!`;G=*<!,XB<G*(,/.G;<('@<G=(.`$S,>ZAJN +M$$QE6%!4_.<X]U7J.;J)DQSE'4)+]XGA152V8G9:95.K--6T<`($8[E972GQ +MKIG:=I6U<^J@0(_NL"B2SJ+VD[DI>CZOUI`(!PJ542\R(5QS<;Y*IW1C$F=E +M652NS\QF>5%2=#H.WRK#SCW5:!)SLBCK-;HB9"INC.^^59>3IWSL0H:[=+"1 +M^Z(K5(/YLK)NJIHWIIZ1I>9)T@G[Q*UZPEL3GV67UNBXT!4$XSNEG&EG%3R" +MT%G92VY)!(WXA4[.MKMF'^IHC=3VKH):5)>%URL!S>93ZF^ZCAO8E*&]BG#; +MSU].##9PH:E%AR>-YQ*NG<J/AWPFWGLURX7Q'T]MS6N',88:#.%PUQ;BG7<U +MP.Y^B]-J?^WN_P#\2\_ZO_[I_P`KCEQD]OBN\0].AKH)P1NM6T(UM:`2%E6W +MYA\K4L_R!=I4LVZ3HWDMI:P7:B(&<#Y6[TP4_+&LDEQX,RL#I7Y`NCZ?_P`T +M?"Z6O)FN>7%+6#('TE0=*+_.?4<##R0`<PKM/_V[_A5+7_V_W6-\Z9G3086B +M7%P@[@*]9AKFB(G>5E'\K?A:5CO]%?B]4]=O\V3NK%$0S)GLJ];=6:/Y6_19 +MC6^3U`00.Z3@=6G.VZ=WYDF_\QORFC?*&^;IHR"1'M*RJM)PI"I/YGXE:W5/ +M^0LZO_[+_P#&%G*_$RX==T)I;TFG)G&5=R:>TJGT7_\`+6?`_966;!8P[KEC +M"PT^Y4PIZJ$]U!5W5NG_`,KZ+HZJ5:B13.096%U2U+R8`RNDK?\`*/RLFZV* +MS>G2.#ZO;.MK[S`3`,KJO!?5X;387Q[2L+Q9_P"X8@\'?\_ZKKX[PQGC]>Q= +M&O&U*;3KS&RV:%0:1ZOU7(^'/RM736WY&K.4Y7%?U%WI_9)KG-=$_5#;[!)_ +MY'+G6XT+2O,`NV]U=8_TR#ORL:WV/PM*A_R6I5B6I4'EJA3FK>DQLK5S_P`L +MJ/IGYS\KG;J-3E.6C1![1"@>V1&_NK+]C\J`_F5-*[VD/.,;[H'@.:0)^B.X +M_*F=_P`GZ(:5*U,M!Q/RJE5AR5H5/RGZ*H[^KZIM*I5&&<843JKV'NK=;\BI +MW'YEK:6!97?_`!5/W*[/I%QIM&#&RX1O_O&?*Z[IO_MF_16I.&UYX+2F+F$` +MJK3_`.2/E)OY?J5C36UN*<C(U)Y^L*&G^9ORB'YOJ4L:5;]I<TY.^REZ;(IY +M,*.[Y16FP^%/HO!TXGZI1G*C9^<IV[JU(,``R("8D:D)V1'<?"BHZ\"7+%O7 +M`W7"V7_E6#=?^^<K>D^K]*I#!&Z/63_4HJ?_`"_HC;^8_`5THR\8@)]<[2@& +M[OA*EL4T"-4#&X"0>.^_91'_`)A3MW;\J"PUY`B81!Y.02H'?\M'0V^B"5KB +M#*E;4QO!]E`/S?1,W<J]ANH/&G=/TXQ3,S]5!>?F^BDZ?^1+\2+#R4QC5,IG +M[_5#4_YGV5@<.`,GZ(I,@]U$/^8%,>/HJD,YQWDIVNS$H*WY/JDS\P45('>K +M!PI`YI&V-L*!OYDXW*J&OS%$MD0JG3':JQ$JS=_^V<J?2_\`W04I%UY_F8(3 +M.=&1V2J_G*&K_P`L*@P_T=IY0.B<F)3-_($]7=JJ%,"%%5<'-@2"B?\`D4#O +MS'X50%0D=_E!4=+4]3\H^4Q_*541.,MX*P.I`T>HM?L"<RM]NRP_$G_-^H5^ +M)]6`Z6`MSB5%<`NSS"*V_P#;,^$U;=2=(IOB8^JJ/<6U<#G=6G_\PJI=_F"T +MARYK21.ZA<09`4K?Z5'<_D*;57J-QMD=U!<L\R@ZF>5/PAY*#%LIIWCJ)(`) +I("O4!#X(DJA<_P#YE_\`B6G2_P">U9ZNFXE:#I_,$^EW^,*4;)(T_]EK +` +end + + diff --git a/rt/t/data/emails/rt-send-cc b/rt/t/data/emails/rt-send-cc new file mode 100644 index 000000000..da8c4daff --- /dev/null +++ b/rt/t/data/emails/rt-send-cc @@ -0,0 +1,5 @@ +From: rt@example.com +subject: testing send-cc headers +RT-Send-Cc: this-is-a-sample-test1e@example.com, second-this-is-a-sample-test2@example.com, test-sample-sample-sample-test3@example.com, + afourthtest4@example.com, + test5@example.com diff --git a/rt/t/data/emails/russian-subject-no-content-type b/rt/t/data/emails/russian-subject-no-content-type new file mode 100755 index 000000000..03d95b8c4 --- /dev/null +++ b/rt/t/data/emails/russian-subject-no-content-type @@ -0,0 +1,42 @@ +Return-Path: <mitya@fling-wing.example.com> +X-Real-To: <mitya@second.example.com> +Received: from [194.87.5.31] (HELO sinbin.example.com) + by cgp.second.example.com (CommuniGate Pro SMTP 4.0.5/D) + with ESMTP-TLS id 69661026 for mitya@second.example.com; Wed, 18 Jun 2003 11:14:49 +0400 +Received: (from daemon@localhost) + by sinbin.example.com (8.12.8/8.11.6) id h5I7EfOj096595 + for mitya@second.example.com; Wed, 18 Jun 2003 11:14:41 +0400 (MSD) + (envelope-from mitya@fling-wing.example.com) +Received: from example.com by sinbin.example.com with ESMTP id h5I7Ee8K096580; + (8.12.9/D) Wed, 18 Jun 2003 11:14:40 +0400 (MSD) +X-Real-To: <mitya@second.example.com> +Received: from [194.87.0.31] (HELO mail.example.com) + by example.com (CommuniGate Pro SMTP 4.1b7/D) + with ESMTP id 76217696 for mitya@example.com; Wed, 18 Jun 2003 11:14:40 +0400 +Received: by mail.example.com (CommuniGate Pro PIPE 4.1b7/D) + with PIPE id 63920083; Wed, 18 Jun 2003 11:14:40 +0400 +Received: from [194.87.5.69] (HELO fling-wing.example.com) + by mail.example.com (CommuniGate Pro SMTP 4.1b7/D) + with ESMTP-TLS id 63920055 for mitya@example.com; Wed, 18 Jun 2003 11:14:38 +0400 +Received: from fling-wing.example.com (localhost [127.0.0.1]) + by fling-wing.example.com (8.12.9/8.12.6) with ESMTP id h5I7Ec5R000153 + for <mitya@example.com>; Wed, 18 Jun 2003 11:14:38 +0400 (MSD) + (envelope-from mitya@fling-wing.example.com) +Received: (from mitya@localhost) + by fling-wing.example.com (8.12.9/8.12.6/Submit) id h5I7Ec0J000152 + for mitya@example.com; Wed, 18 Jun 2003 11:14:38 +0400 (MSD) +Date: Wed, 18 Jun 2003 11:14:38 +0400 (MSD) +From: "Dmitry S. Sivachenko" <mitya@fling-wing.example.com> +Message-Id: <200306180714.h5I7Ec0J000152@fling-wing.example.com> +To: mitya@example.com +Subject: ÔÅÓÔ ÔÅÓÔ +X-Spam-Checker-Version: SpamAssassin 2.60-cvs-mail.demos (1.193-2003-06-13-exp) +X-Spam-Level: + +X-Spam-Status: No, hits=1.0 required=5.0 tests=SUBJ_ILLEGAL_CHARS autolearn=no + version=2.60-cvs-mail.demos +X-Spam-Report: * SUBJ_ILLEGAL_CHARS 1.0 (Subject contains too many raw illegal characters) + +Content-Length: 6 + +ôåóô + diff --git a/rt/t/data/emails/subject-with-folding-ws b/rt/t/data/emails/subject-with-folding-ws new file mode 100644 index 000000000..c0826325e --- /dev/null +++ b/rt/t/data/emails/subject-with-folding-ws @@ -0,0 +1,10 @@ +Subject: =?ISO-8859-1?Q?te?= + =?ISO-8859-1?Q?st?= +Date: Mon, 02 Jun 2003 20:58:30 +0200 +To: rt@example.com +From: foo@example.com +Mime-Version: 1.0 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: 8bit + +test diff --git a/rt/t/data/emails/text-html-in-russian b/rt/t/data/emails/text-html-in-russian new file mode 100755 index 000000000..b965b1b59 --- /dev/null +++ b/rt/t/data/emails/text-html-in-russian @@ -0,0 +1,87 @@ +From rickt@other-example.com Tue Jun 17 20:39:13 2003 +Return-Path: <rickt@other-example.com> +X-Original-To: info +Delivered-To: mitya@vh.example.com +Received: from example.com (mx.example.com [194.87.0.32]) + by vh.example.com (Postfix) with ESMTP id 8D77B16E6BD + for <info>; Tue, 17 Jun 2003 20:39:05 +0400 (MSD) +Received: from hotline@example.com + by example.com (CommuniGate Pro GROUP 4.1b7/D) + with GROUP id 76033026; Tue, 17 Jun 2003 20:38:00 +0400 +Received: by example.com (CommuniGate Pro PIPE 4.1b7/D) + with PIPE id 76033052; Tue, 17 Jun 2003 20:38:00 +0400 +Received: from [217.132.49.75] (HELO compuserve.com) + by example.com (CommuniGate Pro SMTP 4.1b7/D) + with SMTP id 76032971 for info@example.com; Tue, 17 Jun 2003 20:37:41 +0400 +Date: Wed, 18 Jun 2003 01:41:01 +0000 +From: Ó÷åáíûé Öåíòð <rickt@other-example.com> +Subject: Ïðèãëàøàåì ðóêîâîäèòåëÿ, íà÷àëüíèêîâ ïîäðàçäåëåíèé íà òðåíèíã YXLWLJ3LPT9UHuLyGTzyuKQc06eIZ96Y6RVTCZFt +To: Info <info@example.com> +References: <0ID97EGL951H1907@example.com> +In-Reply-To: <0ID97EGL951H1907@example.com> +Message-ID: <HDE46LIK8GGJJ72I@other-example.com> +MIME-Version: 1.0 +Content-Type: text/html; charset=Windows-1251 +Content-Transfer-Encoding: 8bit +X-Spam-Flag: YES +X-Spam-Checker-Version: SpamAssassin 2.60-cvs-jumbo.demos (1.190-2003-06-01-exp) +X-Spam-Level: ++++++++++++++ +X-Spam-Status: Yes, hits=14.9 required=5.0 tests=BAYES_99,DATE_IN_FUTURE_06_12 + FROM_ILLEGAL_CHARS,HTML_10_20,HTML_FONTCOLOR_UNKNOWN,HTML_FONT_BIG + MIME_HTML_ONLY,RCVD_IN_NJABL,SUBJ_HAS_SPACES,SUBJ_HAS_UNIQ_ID + SUBJ_ILLEGAL_CHARS autolearn=no version=2.60-cvs-jumbo.demos +X-Spam-Report: 14.9 points, 5.0 required; + * 2.3 -- Subject contains lots of white space + * 1.0 -- BODY: HTML font color is unknown to us + * 0.3 -- BODY: FONT Size +2 and up or 3 and up + [score: 1.0000] + * 2.8 -- BODY: Bayesian classifier spam probability is 99 to 100% + * 1.0 -- BODY: Message is 10% to 20% HTML + * 1.0 -- From contains too many raw illegal characters + * 1.0 -- Subject contains a unique ID + * 1.0 -- Subject contains too many raw illegal characters + * 1.2 -- Date: is 6 to 12 hours after Received: date + [217.132.49.75 listed in dnsbl.njabl.org] + * 1.2 -- RBL: Received via a relay in dnsbl.njabl.org + * 2.0 -- Message only has text/html MIME parts +Status: RO +Content-Length: 2743 +Lines: 36 + +<html><body><basefont face="times new roman, times, serif" size="2"> +<center>Ó÷eáíûé Öeíòp "ÊÀÄÐÛ ÄÅËÎÂÎÃÎ ÌÈÐÀ" ïpèãëaøaeò ía òpeíèíã:<br> +<font size="5"><b>ÌÎÒÈÂÀÖÈß ÊÀÊ ÈÍÑÒÐÓÌÅÍÒ ÓÏÐÀÂËÅÍÈß ÏÅÐÑÎÍÀËÎÌ</b></font><br> +<font color="red"><b>19 èþíÿ 2003 ã.</b></font><br> +<b><i>Òpeíèíã ïpeäíaçía÷eí äëÿ âûcøeão è cpeäíeão óïpaâëeí÷ecêoão ïepcoíaëa.</i></b><br></center><br> +<p align="justify"><b>Òpeíep: Áopìoòoâ Ïaâeë.</b> Ïpaêòè÷ecêèé ïcèõoëoã, oïûò paáoòû áoëee 10 ëeò â oáëacòè ïcèõoëoãèè è áèçíec-òpeíèíãoâ. Àâòop pÿäa ïóáëèêaöèé è ìeòoäè÷ecêèõ ïocoáèé paçëè÷íûõ íaïpaâëeíèé ïcèõoëoãèè, â òoì ÷ècëe: “Òeõíoëoãèÿ äeëoâoão oáùeíèÿ”, “Òeõíèêè è ïpèeìû ýôôeêòèâíûõ ïepeãoâopoâ”, “Ñòpaòeãèè ôopìèpoâaíèÿ êopïopaòèâíoão èìèäæa” è äp. Çaêoí÷èë ËÃÓ ôaêóëüòeò coöèaëüíoé ïcèõoëoãèè, Ðoccèécêóþ Àêaäeìèþ ãocóäapcòâeííoé cëóæáû ïpè Ïpeçèäeíòe ÐÔ, êópcû MBA.<br><br> +<b><u>Öeëè òpeíèíãa:</u></b><br> +1. Îcâoèòü ïpèeìû óïpaâëeíèÿ ìoòèâaöèeé;<br> +2. Ïoëó÷èòü ïpaêòè÷ecêèe íaâûêè ìoòèâaöèè ïepcoíaëa ê paáoòe;<br> +3. Îcâoèòü ocíoâíûe íaâûêè êoìaíäooápaçoâaíèÿ;<br> +4. Îâëaäeòü ïpaêòè÷ecêèìè ìeòoäaìè coçäaíèÿ è ócèëeíèÿ paáo÷eé ìoòèâaöèè, êoìaíäooápaçoâaíèÿ.<br><br> +<b><u>Çaäa÷è òpeíèíãa:</u></b><br> + - Îcâoèòü ìeòoäû ïoáóæäeíèÿ äpóãèõ ëþäeé ê âûïoëíeíèþ oïpeäeëeííoé äeÿòeëüíocòè;<br> + - Íaó÷èòücÿ íaïpaâëÿòü ïoáóæäeíèÿ coòpóäíèêoâ â cooòâeòcòâèe c çaäa÷aìè opãaíèçaöèè.<br><br> +<b><u>Ñoäepæaíèe ïpoãpaììû:</u></b><br> +<b>I. Ìaòepèaëüíûe è íeìaòepèaëüíûe ôopìû ìoòèâaöèè:</b><br> +1. Ìecòo è poëü ìoòèâaöèè â óïpaâëeíèè ïepcoíaëoì;<br> +2. Ïpaêòèêa óïpaâëeíèÿ opãaíèçaöèÿìè.<br> +<b>II. Ïpaêòè÷ecêoe ïpèìeíeíèe ìoòèâaöèè â óïpaâëeíèè ïepcoíaëoì:</b><br> +1. Àíòèìoòèâèpóþùèe pacïopÿæeíèÿ;<br> +2. Ìoòèâaöèÿ è oöeíêa äeÿòeëüíocòè (poëü aòòecòaöèè coòpóäíèêoâ);<br> +3. Ìoòèâaöèÿ è ïpaêòèêa íaêaçaíèé.<br><br> +<b><u> çaâepøeíèè ïpoãpaììû ó÷acòíèêè cìoãóò:</u></b><br> +1. Îpèeíòèpoâaòü coòpóäíèêoâ ía äocòèæeíèe oïpeäeëeííoão peçóëüòaòa;<br> +2. Îâëaäeòü íeoáõoäèìûìè íaâûêaìè óïpaâëeíèÿ ìoòèâaöèeé ïepcoíaëa;<br> +3. Ïpèìeíÿòü ïoëó÷eííûe çíaíèÿ â ïpaêòèêe óïpaâëeíèÿ ïepcoíaëoì;<br> +4. Îïpeäeëÿòü èíäèâèäóaëüíûe ocoáeííocòè (ïpeäïo÷òeíèÿ) ìoòèâaöèè coòpóäíèêoâ â opãaíèçaöèè.<br> +<i> õoäe òpeíèíãa ècïoëüçóeòcÿ paáo÷èé è cïpaâo÷íûé ìaòepèaë ïo ìoòèâaöèè è còèìóëèpoâaíèþ ïepcoíaëa poccèécêèõ êoìïaíèé. Ïo oêoí÷aíèè âûäaeòcÿ cepòèôèêaò.</i><br><br> +<center>Ïpoäoëæèòeëüíocòü: 1 äeíü, 8 ÷acoâ (äâa ïepepûâa, oáeä)<br> +<b>Ñòoèìocòü ó÷acòèÿ: 4 700 póáëeé áeç ÍÄÑ.</b><br> +921-5862, 928-4156, 928-4200, 928-5321</center><br> +<font size=1> Åcëè èíôopìaöèÿ ïoäoáíoão poäa Âac íe èíòepecóeò è ïo äpóãèì âoïpocaì - ïèøèòe: <a href="mailto:motiv@mailje.nl">seminar</a></font> +<br><font size="1" color="#ffffff">3ZkRPb60QBbiHef1IRVl</font> +</body></html> + + + diff --git a/rt/t/data/emails/text-html-with-umlaut b/rt/t/data/emails/text-html-with-umlaut new file mode 100755 index 000000000..90e5d3fa9 --- /dev/null +++ b/rt/t/data/emails/text-html-with-umlaut @@ -0,0 +1,35 @@ +Return-Path: <gst@example.com> +Delivered-To: j@pallas.eruditorum.org +Received: from vis.example.com (vis.example.com [212.68.68.251]) + by pallas.eruditorum.org (Postfix) with SMTP id 59236111C3 + for <jesse@example.com>; Thu, 12 Jun 2003 02:14:44 -0400 (EDT) +Received: (qmail 29541 invoked by uid 502); 12 Jun 2003 06:14:42 -0000 +Received: from sivd.example.com (HELO example.com) (192.168.42.1) + by 192.168.42.42 with SMTP; 12 Jun 2003 06:14:42 -0000 +Received: received from 172.20.72.174 by odie.example.com; Thu, 12 Jun 2003 08:14:27 +0200 +Received: by mailserver.example.com with Internet Mail Service (5.5.2653.19) id <LJSB7T54>; Thu, 12 Jun 2003 08:14:39 +0200 +Message-ID: <50362EC956CBD411A339009027F6257E013DD495@mailserver.example.com> +Date: Thu, 12 Jun 2003 08:14:39 +0200 +From: "Stever, Gregor" <gst@example.com> +MIME-Version: 1.0 +X-Mailer: Internet Mail Service (5.5.2653.19) +To: "'jesse@example.com'" <jesse@example.com> +Subject: An example of mail containing text-html with an umlaut in the content +Date: Thu, 12 Jun 2003 08:14:39 +0200 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<HTML><HEAD> +<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; charset=3Diso-8859-= +1"> + + +<META content=3D"MSHTML 6.00.2800.1170" name=3DGENERATOR></HEAD> +<BODY> +<DIV><FONT face=3DArial><FONT size=3D2>Hello,<BR><BR>ist this kind of Messa= +ges, that=20 +causes rt to crash.<BR><BR>Mit freundlichen Gr=FC=DFen<BR>Gregor=20 +Stever ^^causes Error<SPAN=20 +class=3D975501206-12062003>!!</SPAN></FONT></FONT></DIV></BODY></HTML> diff --git a/rt/t/data/emails/very-long-subject b/rt/t/data/emails/very-long-subject new file mode 100644 index 000000000..ad420d0a6 --- /dev/null +++ b/rt/t/data/emails/very-long-subject @@ -0,0 +1,12 @@ +Subject: 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +Date: Mon, 02 Jun 2003 20:58:30 +0200 +To: rt@example.com +From: foo@example.com +Mime-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +This email has a very long subject. Our DB allows you to use subject +no longer than 200 chars, but we creat ticket, don't generate an +error and trancate long line. + diff --git a/rt/t/data/gnupg/emails/1-signed-MIME-plain.txt b/rt/t/data/gnupg/emails/1-signed-MIME-plain.txt new file mode 100644 index 000000000..bbf316b59 --- /dev/null +++ b/rt/t/data/gnupg/emails/1-signed-MIME-plain.txt @@ -0,0 +1,38 @@ +Message-ID: <46D7309C.9040804@example.com> +Date: Fri, 31 Aug 2007 01:03:24 +0400 +From: rt-test@example.com +User-Agent: Thunderbird 2.0.0.6 (X11/20070804) +MIME-Version: 1.0 +To: rt-recipient@example.com +Subject: Test Email ID:1 +X-Enigmail-Version: 0.95.3 +Content-Type: multipart/signed; micalg=pgp-sha1; + protocol="application/pgp-signature"; + boundary="------------enigF67974ED650702891ACEBB10" + +This is an OpenPGP/MIME signed message (RFC 2440 and 3156) +--------------enigF67974ED650702891ACEBB10 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +This is a test email with MIME signature. +ID:1 + + + +--------------enigF67974ED650702891ACEBB10 +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="signature.asc" + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.7 (GNU/Linux) +Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org + +iD8DBQFG1zCi0ygDXYSIHxsRAqEmAJ9mKiEdoWg9IFGlUlhPrzo9+1tKSwCfdqRG ++HKnj+81jWpBEhj6D00uNrQ= +=ZFav +-----END PGP SIGNATURE----- + +--------------enigF67974ED650702891ACEBB10-- + diff --git a/rt/t/data/gnupg/emails/10-encrypted-inline-plain.txt b/rt/t/data/gnupg/emails/10-encrypted-inline-plain.txt new file mode 100644 index 000000000..93cc19516 --- /dev/null +++ b/rt/t/data/gnupg/emails/10-encrypted-inline-plain.txt @@ -0,0 +1,31 @@ +Received: by anduril (Postfix, from userid 1000) + id 3AAFC37F5A; Fri, 10 Aug 2007 15:52:03 -0400 (EDT) +Date: Fri, 10 Aug 2007 15:52:03 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:10 +Message-ID: <20070810195203.GB5815@mit.edu> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii; x-action=pgp-encrypted +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAgAyB2wkXAxcNw6+iKwzSlXlyZCsdZFSstJbhIr/i+ujIrk ++NLUAIgPfgIeg1nGVVK1z/hszrHRo3FT2b+jSrPF2Ox/wsUqpSvDf641hFbDCIlx +bbqNmUEOCP5Q64Bw+RHoDoGLx9CPgeXLbciXvPZtlC3MPqG9w8lQdJlhTM1lqwCs +1kqyvQ8YiFrvCNxU6x/81O9wWwiGOVELVnwX62crUK8howCpmeGx28Uo3HKG7NMv +vTsdO6vwh38tNRz0kX05+AlxWP+3Vs4se6YwePc+XnhgWQoHqrYcxTOA0OrivJja +1ty5CAHzGudBdXBcDncg0+6d1Ih60d5JP7nhl3lyzAgArA8CO/iv/kFOkQN9qm4O +4zq189niju7mVCmcTLgBoxN2U1AeAlSl/JacPw4b3CGDQKmj1L/SMKOfUvOvCUlU +o6XB7fHPxsFrJag9Yp5snenMRrvoUbypRljebHYsjtkCOsLqK7KUYLx2JQ/pHJjs +AXwuyytBpLVxwEy0xkujpAQ4rsYT+z410zdH8hAthFo9FwFGKsT95WzfGcOa2+B3 +Qi1LX2uav8q5PAQgbjatp/aiHn3mHZSkRtLbOPr9A8GkmjQaNqIFgUMfaKx2hu8z +nIZmAjpZ8CDvyKoFU0g9Z2KqNPagOqHgq6sRKuAp/5nz62sMyl4Hc7UfeYMTHWn/ +tNKCAYtyAHAGuvwLiRCN3M5h5Y1DdgEyIri7RtH/LW2QLCqYt9MLvmVRsPIEtuEh +tZAb+KQzr9W6+fOhpW/1zkBCGxw2PEhv0HafXsVxEjOITbc2Hfwn9kySZ1aeSxL8 +DXzE9fSwcCOXQE1YY7TVNKuqqm34BADCnO7jw/b8EaOHRWgElw== +=MYSm +-----END PGP MESSAGE----- diff --git a/rt/t/data/gnupg/emails/11-encrypted-inline-attachment.txt b/rt/t/data/gnupg/emails/11-encrypted-inline-attachment.txt new file mode 100644 index 000000000..e3695f776 --- /dev/null +++ b/rt/t/data/gnupg/emails/11-encrypted-inline-attachment.txt @@ -0,0 +1,80 @@ +Message-ID: <46BCDCA0.4000205@mit.edu> +Date: Fri, 10 Aug 2007 17:46:08 -0400 +From: Christian Ternus <ternus@mit.edu> +User-Agent: Thunderbird 1.5.0.12 (X11/20070604) +MIME-Version: 1.0 +To: rt-recipient@example.com +Subject: Test Email ID:11 +X-Enigmail-Version: 0.94.2.0 +Content-Type: multipart/mixed; + boundary="------------010900070701080408060501" + +This is a multi-part message in MIME format. +--------------010900070701080408060501 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 8bit + +-----BEGIN PGP MESSAGE----- +Charset: ISO-8859-1 +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAf/dbKZrl//1Q5r/FjPmqjp2+NDajR7Fj2afjKtkR8ySKni +gc8kTvx2ceQ8bdOMfdn2AngPlKKyADp8xLDge3hwZg/mBRpOtk5B6eqGZDSwDxgQ +Q2+mUVilhDBqLB5uv4rXoclT/JqR/WBWW1SjZgVVaf/lB9mI5amcmOPuY5vBq/iY +q+qBAlWc27AfafGPSz0nifohc2qmq/pj2G6YDkKBE01EZRwbwvm35bTjI6rFL6dp +Zzx2kGhAu928orL5Ft/KtyOEqdGwGlrGApLVV2X6+G1X6ASdgR24OAqnPxYMnhoT +I3uYidSE6DSmUAm4R0rGRc1yMG5rBgRDDOYumRJFHAgA9WsNlQTkSuzypFzW7xT1 +zAzurHA1OYttjjWSAFb/eOnLxc27r1mBS0JVoEz/9+HtHOgGcsRe/sym6uwOOMT1 +znpgRGtQ+vL0bwBK3HyLqQzY9o5BGphf3ipCpI6HWwBDdGUJT+K7S8J9Og4AmV9V +iq6V1PCdGwWS/XD4tulHIPUiA7Z6osVqtbHVTuQNWwsFU06SEHOmhNEGDSmO7Q7i +bUS1YvC1STmJttzSsh+A/dh7uN/mfrPG1BJEAi4iZRGM6GFoKS5CQnpyQ4Az222n +w1iB3u170ijcgLvDNR1Nz5DVnXdJcbVg46GvAGjctI7qWmFIKuazCA/AA0hwRjfz +j4UCDgOxaIPydPr+7xAH/AzvNqeXEU9n7Y7K6rALWKwETU98ltzyp0FdO0YCbN/Z +HWAyazGpPL6VqECWdc9xAYa1zYRf9HEW25yi5BMqbPav7oxfkEMbfLEl2OJr+EfO +/GeTijv8riPFazcXdj7CKuumsld/GQgVLjwmRdoVtGN8WMRZfM/CII6uhg2wVbxq +iThRc03DSNwLr0N0aNr66rySOnvTZNEARljPA1VaRgy1YkHXAyj6Z+s5OBZgj3yh +3f6KZXaRBq9r+7iXJFuOpQ35k/pykHL6wwaTYMmEvlPZ9EO1zgvBxS6NWm4Ct4+X +l9anXjRRPS9aSEUZQRjP6VfQPrrlhBNIgAEoKHlIJe4H/RVbTUAqRlTf3k4XBwkR +HoTk/b/vM5grRQTR12DLXKB+NeNEpQPQFbdFOhfX22ZJbVzl3CVxaOEYHB29KT4D +2frp99spiO8gksYdC5kviXeXXlXINtqsAWCEQr96wOlE03NuKh9TLF3ykAKageqr +yDVdFjmeuMwL5XWVdWFUCDKROQz0gbX3WglCVhccrB4aMPx1vP4HiQIn7R2Q9/GJ +I0lrn/se5GuT078BnhpApZkA0zU+Fi32NWTY7TLXYKxK1BpHWREEFIwrPe1D3Sot +tLh+E65rDp2dt9LrZgGerSQqP1V8z50reNcGdRuWXfJSBwWJfVVo6ApEqD17bPo1 +Y8HSfAGGTN7euRDBuov2eMPKlLTYqQAqOE7DJK1IWQtw9aIMxLuTBCORy+DHh8v3 +elFBb1PxNZ64BJrOK4+gUm09eizynTdtHYhvhe8Gplu6JO5U28djXVqlIiHtEhF1 +VltikOnWlztV/2M8XoUKZneB9MBqhJ3W0lqN5Xzw7Jk= +=bB9/ +-----END PGP MESSAGE----- + +--------------010900070701080408060501 +Content-Type: application/octet-stream; + name="text-attachment.pgp" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="text-attachment.pgp" + +hQIOA076g5OuwfIOEAf+KE8JLsuGPug17A+1JJwCliRR8Iid4CxXyFplTXYgeFGKh2EN0UJw +Rj0k10BKnefbhL2gRM90aQaAF4gBF9mCh4H4Rg1reZ5ohoinNPk1Qgkotx1JnyVxIEk7mzKE +tDIU4BGdcaN6LPhlBRBsSA/nzA19oM8HwecBstws9fbS6ZRwV8XLXKEWsQASEfmJxSYWpdkJ +GEWIprcwkKClXf9RO+FXhFeg9i1ucFC05/1053w1CZtBAB9ohqs/GhILiCzVc8RZtQ+Xig6c +Pdv7Q7zZBRy3lFz3o6bAjefxZfNbvVZmuJGMLPrevXF02v4aGazCtzqm7wR5cH0sxaaW0maD +Fgf8DAeE4A+GqG16s6GGodVllO2JsDWYlyVnf+kfE4FU94uspYteDeGBf4zXhd9ccRYOnDWO +o6yBwgonzgrZHicF6kcYhJLQS0uM4C1cK9S5nh6CP8cZEUUmEar9g5TKQjWivNGl4qNdzm10 +4dZOK1/u5xFb6dkrqOZ1a1M2DfrZRxgdz2RwZZbwre88wGKP3taBtvJvFXk26QNEa78fuoGL +wqo3jvsn1evswD/JL4bVJl6VGRDNAsqSxkR6209W8qseFA1tw1aFM8XhpZzhchOlshg+ri60 +r7fMKln4t1hiaBstROGCCtdLomb4ezxJQddp/SH4K3XdHBJg+4ttjsUmkoUCDgOxaIPydPr+ +7xAH/RbwjSq8A8C7lJVXcwnmEIIpQfaKNaFkSRhp9+/y8e8skhUz6JRopeLnMv0u4o6jos+2 +9VGQ8ei+wzA8NgIbWtnxktT4aobI+lWpy7fyC13aMcG64lYbDUjktW7YNxkMYD9+Pk5bBpwJ +hf9cXhfdSPPofqUM1DmA1LcrkkO+d/qvTCnHjrrjwvTFGBmvKi+iKUk1pltTvOdc5rkK7oI4 +6U6JTYWp9HfvNt7oYCMuILYEftvY0XhBCiUIWUClsQ7WhfqoGpix1peOv4ajzDYeDPo18cMi +oyDrAHAeWAOjYWtuDXOtwpdaWkAoxgZOJgcu2ZVUWdGyFYvSCwOxfZYzv5IH/2a3xIjO21OC +M5ql+lXb1OFfqsqXYXAhOP5+JhBOa5UN+VBIKG9pxVL48mWCK1GwHN5RX5aB+ky6tvkrK1QD +XjCZRxj8o+le03bZ16QiWSEuqxTg2e4/zWY1ZUXBqkxpBVRwT5hzEbLVays0Bju6vKV8CyX9 +apFxFf/vWZ347x+/cK6jc/Bpc8u9PSdOBwGC1ReZixmvgL3fI9ozLOlkNXGvjKxx2Ui6v6LB +e6SBQsD0cnhRGgfWXgJQsmmPDVPbcmM9+pU/p4JsqdA15lNUILXeeiieFNGHjUORgZtjhY45 +Z1Hw8EOfZWBquFbNt5tRaR6UmroviHO4kNx5N39DtfvSegEma6N5QIsTxrpw0IP2NMVXJ9qa +4in+Orrc1EQfljFTLYSyK3zowghdHlZsMUo7d5+AMjDlsjKf9H0f1gMB8hWebQQNlyNOmKa/ +uHMcJVlbpGFCOspBgN4N2+s/Ldz+c9hy9W9x7J5U2bcdsdbEKlMF0EkgUE+f3Jhc +--------------010900070701080408060501-- + + diff --git a/rt/t/data/gnupg/emails/12-encrypted-inline-binary.txt b/rt/t/data/gnupg/emails/12-encrypted-inline-binary.txt new file mode 100644 index 000000000..e07163bcc --- /dev/null +++ b/rt/t/data/gnupg/emails/12-encrypted-inline-binary.txt @@ -0,0 +1,86 @@ +Message-ID: <46BCDCF2.3080704@mit.edu> +Date: Fri, 10 Aug 2007 17:47:30 -0400 +From: Christian Ternus <ternus@mit.edu> +User-Agent: Thunderbird 1.5.0.12 (X11/20070604) +MIME-Version: 1.0 +To: rt-recipient@example.com +Subject: Test Email ID:12 +X-Enigmail-Version: 0.94.2.0 +Content-Type: multipart/mixed; + boundary="------------090206040704060905090502" + +This is a multi-part message in MIME format. +--------------090206040704060905090502 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 8bit + +-----BEGIN PGP MESSAGE----- +Charset: ISO-8859-1 +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAf/bD4qivq3N/0qZZnLucbOARr1mypqomT+gvm1It/DllR/ +F3RRLlm0wcBoVQp52aNecT9OJiZNh32oxPW78XBG2OCZv+jATd2vMf4ESOX1Pj4+ +1DATM9V3lOJ8XYJqAX9dVe3MdUntpwqmGvlv2bYmbrc1o6IVQroPm6eV+ttB0BKc +ELj+1NjpaXDGERoV2P300JcJNR6tu9pXo56kz1vAjAQj/xQBNCoSOxBB1TJA1sKF +iNuQVObWYG2cE63PRZFTlzvTofeHo9kt9ykfyfNMnslwAaXPI5LJP3KGjUOZiomq +XA+FYLzMuFzHaOgRGcpJGKTeZ578N7WZ21zmNOgS8ggA0/RNae8CHklYRd9sySdo +0vxoAO6pokK9HSCaxeX96nS4JJfij/uTMRyR1cbwFBvJSQHUtGNGL1Tn9vhgGfqL +TjrkvkC/HhBqu4QDYH3ekobV/tu/uQjGhxAASTFB5vtYSQvZtmCR1COTmO559aDz +oO+93N4x3AOhC1Nhcmyds9mSCKCt4TCWE3gmHuPKYuLpSwj5Ndi5er6jCsq7RpsJ +W0GzGOGg8wUUPDgBWJbXS5CVAkiJs/SlP0yUqeVR18mf9r/oKUt9DL3GCp230/o+ +0wyVkzqOBPNiIHpFRN1NhikXdKRVOL4ewJeCc9nrlBtLwqpEyZ1g4Ht7SKkE3rl1 +64UCDgOxaIPydPr+7xAH+gNwaV49PwLd8WUJpPuL+YmH5RRsAasWvX+VlnxM+g/7 +cn1BrvpXekq7vctMByxHaV2WnaC42n4M8xC+V1B/7Wk1rmhRMZKpIVjM6X9eNsPg +FvOX5DhJdhgNOhjwT7rk29P24p9TQj06TwPnvLYjQJ6LViiX5l4T1YpG0mMb6Sc2 +vmp1MbGeV8zZ3hVbSpWOmwlotvfiGa4LjLGNduXIiOFF93pviV2xApYNt+FgJHLc +p63GfqUXuj/K2iqKwFIbo7nssx/ceWBRdlgPrE/MSStYhBm/e9mw55nwDsUIB4Pg +tkle/6XgRsOgvHydiTD+mxng/1bx9cmvVtNfvOq82wMH/3JI/rfmQUQGRJ/JFufk +Kl26lQcbd/Vyxff3MVww7oUFBY2oUPwYL/5N3TjhI7HwiCkMP6S9bL1AfHvkLenu +n5GKjSLJmyx7BNQMEIIKWD4tNSjA0CCk3+QVGr9sRIVIVD44Be9I8jlcpkn2r5Qs +9djSXzEBpM+Aiqv5cCBFq2rgYyd9m2c2iHsI6EBaxCzYFz1qobsfvXk4a7ax0/Ck +ug/jPEq8GK36UTp+51G8JhxzgJKSm4Fo7N3Os8emIlZ2QymPrzPkxEQj9eRsMmp3 +qIQ0rOPjTC8847s4lIlAx7di80OYQ1dV2J8NLL/qxF09rr6f978oH59IfSHRKJ2b +aCnSfAGfFBr68Zm20se6/0mzrYY2MDljPrKSDQPmNW7nJ761/YjMgVAeZlnKW7JM +hTdZciuHMTX7+3BDTGCMbAnmYYWurX0jKGJ4X0UJLV8m4nsXvqHvsG6FBzVC5z2w +kytey5Bqi2gXRjk6xqckH4O79kEJw0kLkxJbT9IPBKw= +=vGEb +-----END PGP MESSAGE----- + +--------------090206040704060905090502 +Content-Type: application/octet-stream; + name="favicon.png.pgp" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="favicon.png.pgp" + +hQIOA076g5OuwfIOEAf+P0Qp/k1B0WDRr9bNcEANStTaiefYoLrUrtMJv+aFtkiSqKfft0A9 +okrYkVUKs6kxfgxueuqNMFQh58nl8+d7Z2qGIgVEXxC8rRxexEQ8mXu5LXzzBbc6Dq8Jsa7B +bXzwGty51culYcKeMEjpEY8Qx76qoNQDNCuvth1JJxJ6xQix/pVyZZbJRu/nLrv1i3Z4KRFY +qafnJlcsUTVj2o9dLfeU13z8nd0uBPY+hiCgYJHSPDLr+mkA+c6YK0m4a88r/wjLmsVHMkn2 +N5nCjuqP4tzT8SCjhoICGTbu+fFdks9NhQjvsW7MHBi9HFFzm6SoEvquFHThzwMl3hAhTLpi +Jwf/a6unMP/swAxoFTJ2GRXBmQOH4sJHR/M31rEVkLZGJixhU94Tpx8ptgLXqme5VCXgl+M3 +Oh0GHRXqFYjR/HGUTZokRKR/BgCEpOGlH5FcabHiu/Gy8UBezPbuC+BNvxuCbuwODMp9R5DE +F8RSCAQ1hrRoJjeHT2wyE7HdCvN/xx7NyenA3GdJa5Z6W7Y0gshr2fAOFL39jKXw4WwCh/Yq +XnyG8uOyPgFrnHI3WpO24VpQHp3MBKebWNhQ/Opy/cABunCSwWQpDB9Ar4GeS3R1WGtMNC4r +ph4afmTHJcQQkoa0VfvHL0hEzycwysYD46O9QhZfhxtKKShgX356oCeMEIUCDgOxaIPydPr+ +7xAH+gMnbi5OLPf5xMeZydvNWdHE/wJTub2rrWFtzvj0Aa5Ne/KFhcDDqSjaL3MXP1WfIJr1 +/ANe1eWcM2hlYVDpEn6YOh0bz6BASE4kbHA5nGMyUrgH0hWfcOgkMUloRZdf1q32j80mchCJ +rF4YsQ6EndnUYzAiXKHGJRUy/6IA2qBH2n/fRiyC2FmmQPtWO4c6t15Vgh4fB3QXSTri8J5r +577yIiHRE+dq6gg1BfyqCtw0DW56lSFQ7dxyMXeLyTGyjTGPlUDc+FbP23CRK7zDIVujARmm +mX3bP2lMfCK326FwBZf2Q4Zl/ac1BN8Mcb4wwcnKvRzfEw8d1Y6pkphe7KYH/0MDDqtmuEw2 +D/xdw4FHB16/HW32bcPaMVvFuseczEfrwPGCrCiPHPm++edAoY0rWoBtzHVpgN+s5bset5OR +snhjuWceuCb+Ga0QV0s/xmIPIQ8VYaXyD5hob6nHEIeskS68Vbni0BpY3nejDPoV3dNHY2Tp +2fjYNHCpsdTz8yyavQVixoQjZQH9hUb48zZDHCt0Af9Rfq6Et5/Qr8iJqAyEU8JzZtrkpO4O +HgLU7JTPLxOGzYtOj8JkLmLguVA5kOafAuU4OTEU43utQfS3KYbdEWT2jJ1QaJVS8CjFJrqH +V99FUsDvgKWSTy5hA9gkQAQE1QdwkoQKpkCWm18KZqTSwNkBfrsEuvHC3Cz4Sy+cJmhr4Hvx +dyZY8DuuWExUcVCjkeuASLgjLEgahnCbMkyKATazswwTEdfzjcOowjLTdaWFEN/Cg22nF/px +9MXMtzBkrTkjYPhfywETKoMVH/Nw7rRNZhkOSb5WJV5ynF1BlbzXI7Z8rA/KrIn8aydzwJUU +qFr8Dw1C7kbE76+SFVWX8fqpwGmQhDAO+kos6ivgN9HDHtuXmwfGeROi2U0WcmFGbAyLo5fT +LCcNPOMflU27WDXm8m+tjq9naUynqvwg5zBBz/xY67L1R8uOwfZplvRi35iZAJjzMHGirkiB +W3ZDXbDqEfKl4aCXqU+XhQZsku2z3OtKZOBVVI5p8nGVEfavg6QECRUNUS7qbtMxlj5IwCGl +babK3W5YVuERjklrrLUYZjqFIZ2yLK3Z2VmSn7yKAb/eRvdEeha+9PKcN11pXPkS/M3t+Vpr +G+4TqNgqwLVWMvbENp08dS3OAPpZLDnqG9CJV0qacDMjv69X26V3Xp6vuZoKqAPxMG9QKAfX +E9LInR1Kd0cpRUkb +--------------090206040704060905090502-- + + diff --git a/rt/t/data/gnupg/emails/13-signed-encrypted-MIME-plain.txt b/rt/t/data/gnupg/emails/13-signed-encrypted-MIME-plain.txt new file mode 100644 index 000000000..f0bd624a2 --- /dev/null +++ b/rt/t/data/gnupg/emails/13-signed-encrypted-MIME-plain.txt @@ -0,0 +1,49 @@ +Received: by anduril (Postfix, from userid 1000) + id 19FD037F61; Fri, 10 Aug 2007 16:03:35 -0400 (EDT) +Date: Fri, 10 Aug 2007 16:03:35 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:13 +Message-ID: <20070810200335.GC5815@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; + boundary="+nBD6E3TurpgldQp" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--+nBD6E3TurpgldQp +Content-Type: application/pgp-encrypted +Content-Disposition: attachment + +Version: 1 + +--+nBD6E3TurpgldQp +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAgAhFFX4adckICRP4VEhrElZ8rRYAwJVbTLd7S5yOnrovcG +sRAIZTjEmb8CRPX926hm3fJ/5O+pm9ZPE1wzvEBQGcJ+6oZPD4GMwuMqJpaZLw4v +6zj4ogzac0i32vkwLYDLHXle9ytgiBwTFHuOKllCvBmTe5PrFAHqhA0EPJ824gD9 +H93LJUyvn/elOHASKVcUhm1XLV0RxdNTh8Afc7dEAIr0uPaWu1rVatdnc9JnQjsB +luYLP8M+UD9u/sZwSBv7x/PVIOE9QsaFQGQZTYEzKb/4zeujcUyM0+4rjJ+45QUX +cDsrFVmXoQfts2nw0BN5mERZdbOIwvkhZMZzsf+EKgf/dA7x9rguO/eGy/keQf0f +sBHAz24WGWRqcmRNdBsaecVgAsygAEi564RQYvxM7eJxqKl+1TsjUmGUAmacShN7 +JjpqTH9HRrV522UNvVXFitel4Ri3UItP3zI+951x7YvkzUtIz8gfaCOHC71NmPBO +RdKDDBYDEajJkYN6mhL+QpX9fIIP5ALkfVz0JmdHN1e81Z5myuEWnCSuxeyZHCgl +Xw8PuV/Af/+GHqjNUaXRDxN4SWm82pKkK4rxioMI4liI5zuR73GH8wd1RjspCkKd +gPEhmqDxvkxykjhtJt5Izj+iQsyZNRDHRkGJA4BLOLUnvFtwpNCg6DaRDUOBjBmI +P9LAXwGbhZU7uZLtSzwn1gr8TD+cqkpQrFlKRiUCdh6sTwfp+HE3JDmKAwX6t8bM +ndBMYwIPddYdkFFpWOQbl1G72zR3SSuwgyCC5+xZxDWPHT97iCKbvCYAmdN3cGHZ +8Lqu56ulz54pIkBZzSsu8TWzZner2eax1MqITH3WNYYuH57yiMSaUK1DSdFrg9Mc +o/aK0QUaW6pCFbYGZRS1cqUBaN11Z2debMC60JiNFA/htzLwgSwmZiEdAIt4lvU6 +xMdA7dFYuVLmVh9+VQYp3KspNDWsILiPsKbV8oomlcLiZ+g8cRY/NzLKXVqX2a+7 +Jl3hLlEuVKdcWe+XCfWt07Y2Ibwtpkq/vph61cPdus0dgV8U+QITQ4kt1ky8xNBc +L51c +=zSIe +-----END PGP MESSAGE----- + +--+nBD6E3TurpgldQp-- diff --git a/rt/t/data/gnupg/emails/14-signed-encrypted-MIME-attachment.txt b/rt/t/data/gnupg/emails/14-signed-encrypted-MIME-attachment.txt new file mode 100644 index 000000000..8a70384a2 --- /dev/null +++ b/rt/t/data/gnupg/emails/14-signed-encrypted-MIME-attachment.txt @@ -0,0 +1,51 @@ +Received: by anduril (Postfix, from userid 1000) + id 061F037F56; Fri, 10 Aug 2007 16:12:29 -0400 (EDT) +Date: Fri, 10 Aug 2007 16:12:29 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:14 +Message-ID: <20070810201229.GD5815@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; + boundary="N1GIdlSm9i+YlY4t" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--N1GIdlSm9i+YlY4t +Content-Type: application/pgp-encrypted +Content-Disposition: attachment + +Version: 1 + +--N1GIdlSm9i+YlY4t +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAf+MEje2ZVc5yRFxINYBmz8l5oRdrDkldSFISKNlcFogKeA +ZAwvP0Y2Utxjs+aT25gqR72uftGzCLhKdIb8X/i8CswKHX/1NnLKFTFB+T3Mmwtg +3a9jiB6kQ5haWvcd9y1DBcm1Wnw5B51QmiHZzHu209dphl7qnoak79M0NK4KPSqr +NKqn7yTKFOjJ3qIlCLqawQbQy++IFmtJVKMQSNZawqSX/JSGMNPz9hioyOgyjxtE +XZlrdgV1jumnZ3nVF1bQFMGUMGpi6NAA7+TiPjSZgFkKp2zvyOXwFKjKqkVgAgUh ++295JO9kjFmRYOxpWcSfBaibX4qFSsWvJbvltNceDAf/WCCcvP6+JYHmZL32YL6+ +oSM3SnHuG3PIqaT0/99pMVIhHzhrdrmWsyR6ijcK7PRpQg3mIZDd5pzAgcae3c4M +mYsImADyUC6CX0PvSOYYvG7CskDUiGk76fjDKTkKPkgqEFJXz1v9TA+ptx9v2PTd +CW8cES9Du5PdwPLbltfC54ax/tCKcyyHbtgozVQJB6RfkXKhUjYI632qnXa7BDqO +c5Pcpn7gKKik4MkGEWnf50aqQ6ph4Q9MfOZ+5hc/EBUQd2Genqn8iCTPa2rkrI49 +6sKJ9FSdXL4Xdlat09MxnD5Dw6DKF6G6Ig2SAQA/WBG5+sZxWC5NMd5oeJWHVnCz +VNLA5QEIo0BNtY+wm2W/YH3hSHV5K+Mz+MEKEUaimYSLWKl1p6vboBjHuk0XY1Tn +FDGmLVcO/jLBkiPOhNyibRXaCW/x3iws+xCHngjX4rDeidEtphHCxihVtS1Sh1wT +IHHE+Kl6jlateIeBpug79RsGKcdwYTUg2SuaZXEf5A2tQtUhU6TEWYqhFc0g2dCV +u/lkhrVRt4S+En4DYJ7myQYdMBeHym52Mz9tFZKKQ54dMWDhDsRZIe0Zy8TKBwXd +cwc5kMpkwFK3drbhFuf3WnQUUR80oyGKCffJw3n40ET91b+nAYJ8bPvYFE+gDB+S +vHktXvM+vk4PTAKGN21TyZfB9n8VVtIHq3DUPpStrFFQQUDKjMeZf0TDtEWVd1zl +7jVJ2d+Su/KcAXUITXAQMl4ECXMZ3VD3fnbyRAHI375+wcBh08rM1DQRdhWd40XZ +55rzTy6T0RVa3JcQBCtM8FYubYQvy8iU5XcA2LEDWdDn8D89IJ+UAnFHIYoeipj8 +qbYjl9JetF1gcmm3QlUwtec6nb5VSmWdQrdfnvdxrW54J3gqlGnzCQg= +=7/EE +-----END PGP MESSAGE----- + +--N1GIdlSm9i+YlY4t-- diff --git a/rt/t/data/gnupg/emails/15-signed-encrypted-MIME-binary.txt b/rt/t/data/gnupg/emails/15-signed-encrypted-MIME-binary.txt new file mode 100644 index 000000000..55f69a19d --- /dev/null +++ b/rt/t/data/gnupg/emails/15-signed-encrypted-MIME-binary.txt @@ -0,0 +1,60 @@ +Received: by anduril (Postfix, from userid 1000) + id 171AA37F6B; Fri, 10 Aug 2007 16:13:42 -0400 (EDT) +Date: Fri, 10 Aug 2007 16:13:42 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:15 +Message-ID: <20070810201341.GE5815@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; + boundary="QWpDgw58+k1mSFBj" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--QWpDgw58+k1mSFBj +Content-Type: application/pgp-encrypted +Content-Disposition: attachment + +Version: 1 + +--QWpDgw58+k1mSFBj +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAgAiH54xEwaxc/DL+nyd5f/77c6Vk27bdxRz1eXHVU0ofqs +oeZHkEWpkub80YQL1HOQTabAF3ZUIGfyfGufnqXpWWNjhnxP/Bi/Itxk4p3oq6Pj +RDoDsrO6SPcM1wlbz7M/8rTmpjTk0cYl3nA9Bc7ZDP7jRWRRKFp3Jl63fvK2BWG3 +x0yA1WwJX/gzgsRlFSBBFukaffCqaeQZJ2GSKGbuHTH8EYvGDjqALlEwE/iPYRYq +OoSa27KIzDCwPFJf8/ZK+03qX2D2LrEViflbFlZMcMGrIFmU0BLrlBx8Obimu3lm +8bCDX5zQsOIptLQqrhQ5E1HIeEFHqJAxJwHu5v7AFggAqSNH4adYAxz3vZyQ3HdV +IiDYdwwGh4usb7R7afc8uiFGzxmcc9ktyQ8bBAEpGHMU+ahcUMcfg1svcSKKFQqS +xNJb5rl3Q9D7qqsoFnNdVFGIxhnUpjt16TdFEtRHDUnC6dWjxKw2fzdR7YsclzkP +IjUP1yHRK0ees3r7LGU90zQUcUXGnuX4xbMi3+t8NFPqpi9DIkOKS77XEpLFg+Yg +qrMi3/p72jGBcgbchyS5JJyrVfZjz3igEDy4A2rEw9lJvyemahpccYf9v8LjVCn8 +ofdeLujIyDV1T+wFuB2gIY8fbnvP2UC+ffJ2qGA4uDiz9T1t+IapBjwwrhG/P541 +KdLpAfXAJzKb/gnXdicy/TyGnMguVXryCVZXAx392jA5se/OVjtVkUV8hMcqBPFC +6zMEPZDXVaCZ7r4VX4xK1y7MGTrq+t4v8UhmfxckiwDVrKZkt6MOwdhTq1YBwFwq +DEDN391R2fFr08i+3ogKv30gNlqs1gfqh55uCsZMsrtjMAEnM7JcB6pnptND/mRm +9MYsz5Sxra5L59SNNhJS0GjuTVGIaEhzwusWdFTAeE358AqTOTwdSNe/Fa3qEXBr +YzmrCgTn4x4YTn8IDQkh80It7ENEc7tkgRN/FcG+Q6cwHWgPOj7QDBJsN/lm8V3K +cWKVxwvp+hoP8isigZxi/7nOD1EZkQkMAWyZSFp+iCZH14GcAKwqxp9Y8bgSuF/5 +jzASoge+POvEIpsbS7lmfalU8qR2kCYg17fEvDH+6olqRCT6Bq30cVUYY+LVAHgz +KXgJjK3WYue2XQieGaNYjtwr9AsVl4fxpjXmb+QwL6H0K4JMdXc9tabF5j5YFeTQ +rWYzED39EiXst9dAFomVhXDHD1OdpAbNh5F9/9wlccgZ7co+tK3bdcD18k2G4XGq +1AX27utveaSU8M1jeHG4a9//f6NLn4cqJ8Qryv0uiY7N0iiwWHsoHrci0doMQI0U +glPhkk7qjHf7YAhsRsybfdNrum4jPHMpk1wqY4GR4xhV3JLAbDfKN17yHJWR3c2w +TQ8sOLMPKMibyo/KRBLCz3CpnuSvpc7A5tCenDJLYtDhmUMofTN1ki3gBW3OFQpd +zH6pCuVBQDQ3iLO/lg6Y434fPz3cuKnxBdN/QvdbeiX4H7tGzC/q+qXpu/8Yv2x0 +AVSQkrXcc4CQXAvLzqNMXa6NgKrVtVNXUgHyIxvOgGyVxULKDQo+3bByccCOjNOZ +3gN/JnU2HvEk3iYDYPa+VOJzS1i6itZOCeCBF+NDaaTvG30owmINGCGl/nxv+yIO +9nlCF3QYdaod2TVYfxdp2X7hlPEhv6nHYt2r3/pXYW4Hjy5M/mT7sR+OVAgknpiJ +yOzeNy/dVoxpAAlOuzwl+sYI6TkDnF0vduJO0jxWP5+oa+Al9sWr4x4E59OkGAg/ +lQ== +=NbQY +-----END PGP MESSAGE----- + +--QWpDgw58+k1mSFBj-- diff --git a/rt/t/data/gnupg/emails/16-signed-encrypted-inline-plain.txt b/rt/t/data/gnupg/emails/16-signed-encrypted-inline-plain.txt new file mode 100644 index 000000000..5686a12bf --- /dev/null +++ b/rt/t/data/gnupg/emails/16-signed-encrypted-inline-plain.txt @@ -0,0 +1,33 @@ +Received: by anduril (Postfix, from userid 1000) + id 98F6C37F69; Fri, 10 Aug 2007 16:17:42 -0400 (EDT) +Date: Fri, 10 Aug 2007 16:17:42 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:16 +Message-ID: <20070810201742.GF5815@mit.edu> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii; x-action=pgp-encrypted +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAgAsnA+yHerSZnvxR6TIiWSIRlohPBIQHUhoeHBH+9Q0DFJ +r6in5UmN1luVGtT8kl1dbLAnoVu2Yyf/d57IWgam41MbWi1EmGLQaaLHcIbQ3JXR +MmXXwG7PTHWdZJismmqCOuT3svTlkqerIBicsHXvfKE209y+jP3lruJ7cVpxZyCI +M75H8d313r3MwBpoRuEiNMBOjG6MXOATFmgRw93J6pzjWirxwmawhaSNeghkO8tN +3vdDkzZlLmM/Pq4jQrkWbIbGH/EwdchbWNnhd91o/Lll77fshkXNMYQlgyU154sk +3wCY5IFGJTUdR1hrETz2nOASLDHIdamhhz1xMItClwgA2uXkVhG0Fslp/A7z4a09 +ivX/gM6a4KuFSNVJtHrlc6Z8/WKe1LNdLiulbFMtbppvMtIkzgZfv/DBavZJBqVI +EOI+9VzLb6IdqybbNp54nRbniU6aiboEk90waSjqHggCnk5qnOUxrxp2ZCIn9pwP +KUisxy6cAKGCEeFdvtXIBqfC6uITAu9kNq78rPN64TUaZbJs8VshIj1zpGi6DUYa +uPvGPIhlLi5xN/oK3Yu2TDrIPxO4m1ZdHQJH8TBn6l6EifAVsDeskSQ4nCZJzc0H +0D+ofkCzxqRkjDY0IOcl7hI79cRxO5tagtf6od0vcK883wUHxe6Kfv26tKm8IsX1 +ANLAFQFi867dLG+X9wc/QyBwNXZ1hPSE8MulfAYyzT/rXno4nkmQrw6zFLf9q4gA +Z7aZsgxXPAvWwhwLGyinNgi0ua5LHZL17CWOfG8/GlIK59mesnk8tC9Gyj8aLU3/ +ROteGBBGfk0UJbfcQ+reAjmORofZdHSMCsGYZ5DJEy3KIUrNHzW4yYAlfzLWYfX8 +k9R41E0xGBuooe578MTBtVOPZKY4gurQZdfrHdYnsUXgfpV1w6WYvEM31n90Px6m +aXWFWq9JVxX/JFOmWJV38fw+EhNgApncTw== +=J8xa +-----END PGP MESSAGE----- diff --git a/rt/t/data/gnupg/emails/17-signed-encrypted-inline-attachment.txt b/rt/t/data/gnupg/emails/17-signed-encrypted-inline-attachment.txt new file mode 100644 index 000000000..6991f0b7f --- /dev/null +++ b/rt/t/data/gnupg/emails/17-signed-encrypted-inline-attachment.txt @@ -0,0 +1,84 @@ +Message-ID: <46D73CE2.8060202@example.com> +Date: Fri, 31 Aug 2007 01:55:46 +0400 +From: rt-test@example.com +User-Agent: Thunderbird 2.0.0.6 (X11/20070804) +MIME-Version: 1.0 +To: rt-recipient@example.com +Subject: Test Email ID:17 +X-Enigmail-Version: 0.95.3 +Content-Type: multipart/mixed; + boundary="------------070807070206050202070908" + +This is a multi-part message in MIME format. +--------------070807070206050202070908 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +-----BEGIN PGP MESSAGE----- +Charset: UTF-8 +Version: GnuPG v1.4.7 (GNU/Linux) +Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org + +hQIOA076g5OuwfIOEAgAn0IWP59DHAWRYz8MG09D5vp+V3rfVdwv8ud2hp/jAUDZ +ogXobK3KkUH5CIDaohqgrxobAtU9D7XhxO1ti6G9ana7+u2GiTIQmC7F1THSipNJ +GZajUNG4E0WQXjvkWvDgx2cLdjn+L/i4Y5lQvP6CEmzZpdfKQk4DZdmMJf/Fz0Ag +jqPI0weYSEr8YBz/p0bEy2Xh7UPw8rwC6ajk/v/E/SfZXI+TpWnFxLt9OUN+o0E/ +o8RQ+5LTpPvkR4RTFFnqZAKu8CU2LqNIWYlzhm67pi+QqeepMuhbW+Ix8tt6oBbN +EXFrwBYfjLeLcmMJcm2fEwE7otDqKHHW5/G/lu8RBwf9E0TjxnGbpTH3ERrj172E +T/LT0LD2qRitQQCdFyeDvnq6KKyoUtkyhwrrfpDfB1ZYBSjgIB8rgolbO6OxnY8s +O6dob+07mQrC3EsbrTQhRjwtLCWB/4kaI0d/9Y4/InStq14AvW0wZWX3kukUq8Us +ReJhrDA/fOV+duOQgcEc6ZsMjLE/snQv6KMN7ey7iOe10ejLs9qHFZClqHhpjbn7 +zTPRapgTOV1hwBWq9603NDP1EQM/wAOFCw1TDUnhFOzUocpBfSwajWY1bKB4pz94 +Nf/U4BBUFq3aQVX3g4mh4sBesYInZ8wMq7fw9fegyiaLL7YzTbxg/YSXHWkTaYCk +YIUCDgOxaIPydPr+7xAH/1BIgIWDNzXHWbrmi2pxg547LWoJ3EpJQ46fk2ryfIll +6Ot1xpCXGStZA1bUnRB0KTZlTNVfXkIy2TuFKJ0xP5JgkeeVQ+Vn8wCXyPB1nYte +WJ9aKHIk9UhKHhW6FHIjWs3CYjfmpJPaI3AXu4hwT/W1yPIFbAb2UYwuvRn5XABk +RDxauFRDoHKPvy4IsorGBPIa5ZkJxuBsP7lxp03CfgnUHX7HYehlNp+rKzp82uLg +fXJdwQAE3OcrnB7rbD9AirIyJpy4q/sYUOnC2M137PF+HYA5zCxktpnkVDtWd6Te +DyQy7T5Ey5/NzP8IbJpS3Wv+bYOiozv1zNCZ4ZbopbcH/RKMlLN78fSTZmgC6Snb +dcwZqi8lvL6vx83VBwUAaQeNDLvtlyjojt0UuDWUI3JQJpv5vcGdLjP21+5f/INt +lua80hg/pKwoKijzQPPtnPBJQOiRkzIEYlaqMkmPi+jgkosf0kcAWxnQGt0L74tD +Q4ENpgCK6jJVkQFN9Gd2efT7EFUc2acY7/YfFcTOBVm6xi0FXoa0HzL4NzMhbrsx +NhlC7/Yp34y3NHjzVlMIKiU9kbHrVCyZyXKuSSV2em0a1SqbJu2wj2/qjK6Ibtts +0aowVn5HafWR6jm8sEEyX/v8c1BdR5Ibmup7Z2DPd0j+fY65GK0nNYZ5QeYq7PxR +rSbSwA0B+Ot9P47q0c2vGrQF7EKvug2sgnE/9K8DX+Q5IVwMQEoJUZvKf66x7DTA +YIMnWxlJeeU6fkURzeBpv0TzidU6/KRa/2xg/QoUTZvftAoaQidsajzFSIZTBEqC +xjzLnkqlDMtlZYBtdvVYDxrBvj8u5whGnhe6GhO8GUfIe/N+bWM5N1kbUAsE7Tau +ify77kszQdf20rLVO9eU0MjXyvKpWcT/Dpk/Nzth3iLACEtjr/dXo5pMwK0mofx1 +I4lLS7P5PtTz4Oat7aV1up1o +=LNkV +-----END PGP MESSAGE----- + +--------------070807070206050202070908 +Content-Type: application/octet-stream; + name="text-attachment.pgp" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="text-attachment.pgp" + +hQIOA076g5OuwfIOEAf/fLTYMB41pbPXfa0LaNlQ5PVkAa8i9W/RVfA8s+BGgFYz/herlLi+ +eCE9Ls1RoiEmh3e3ZOG0M/iPA0qBMryH4fcYWqx3R7uaZDVIOEcUM3lR9Q0ffNfGRyVMKX+N +nUUI/HAaSLCl1NWF6Qnh+/S8tV97D6FDdqQqJKeG7HtPcXMJ3daqHcCl5mMrL/OgcwEcqdHl +dw8VnESjnkQ3NAvzyNJEpyB7fHN6HtaynxmZbU262ez9Ywh8XsfBbKsDhnLRf/rEUt7X3RQz +YjXL2C6scGT9ctZ6i1yxPQJOP1+Z+UxijfSd8S59n51/SATsdjmhNFst1JZTbX9Xm/Gv//mj +aQgAuzhrqVBvyOtX1fMNxGACBhMuJKMLvb9bCltnl4V8IN76d1HP65bw9FvFC7JNcLnbeLBk +6J6CGhwZYcQHuLBiTAfycM636RotiszjLTx1/7j/Jp3vvzToLm3sNNb4INVmoRBrmVqqP2wl +Ykl7mxdHAq0Lr6NMiTnk9/TVbJnXdCFseLONJaX6ZPrPZUbw6jhgDMeyAjoBkHJjGqfbde2S +TsHvPvT53g8d2RgZx5ATptTphU5QsBbS0eYEIiKoU4lUh7augb6EratU63xTsKc9pJ8DJOqB +l7Ic1ujUDQqlccTVJgYyIJJVO/GUR/AkIYrzWcvEwhtk+PW5CMwrcbEo5IUCDgOxaIPydPr+ +7xAH/RYdKbPnEo/0R8mZls6xuycjGoGNWYuhhaze7NiDPuflz/oMkPL9f/o/7a+9t5mBbdQ9 +XXnNKuhclocbDg6N/TLXWWw9011Ba2ko2l8LpvzlLo7oARaMYZu4kmMYZIkIeQ2GW97gIV4w +yXLJOuVnH5cudt36/P4QqJM0PxSqmQA5sqAMB7cvgK3RFKbDvvrMvmyrdoTFmrcFhKMWbTq+ +g51hVLm7HEHjicP6LEhHv7m4ooe+rojS4D+0HofvkPou4XWJ+R2T4CVKOSfobrp0RjMqjRuJ +PvdzhyUNjP1qhtvUyM0JBgYHd7biFKOKjYG7E7j9+5EhHG0uSG+xRWq9NtgH+QF2Mhb1WB24 +lpXZM2KPM6OdStgEJFEzjr9DwULJpEzQhvOAUI45Jd7oqLQjEdmzuPmkEBdNfcUBKkCNdADp +JBn/0Ak2C4OQImtZX6kyJ2afF7zwMQ5J1eH2e/eOYJTOAVnt29EH51UZHi6OCWosYQhDMNZW +8AXoza4H3474bPzlxw7utSRm/CJwetrmp3L9BahuEheYv3PZCmy/1aHpt5OHIJDmrDVMzaAH +DhWNkRuY4/CLvg/+r4W21Y7tsPllPGNfxLJt9kilu8F3M/alhNK8CxbM3sskNnxSPPid5vE2 +e88iO74uhwvQzTrPKlXfsUDpnGacAwG3uNS3x3CbimjSwAkBjvCRETU1DziOU5xVTCGEQFPv +gF3YtRG7Bmvm0d1zeAlPDuflRhadYCdNvy17gA8LI+YBuk74XavAqKURN6FEevULYaSPZCx8 +HXgjE0uyo8U7l2D9hrYjGuvON84Cwp2O6+jsiLk4oVvdBVbb0PRz4xj/Egnlu+yjGD88Swge +zpQzBdaqE8m6sizWBlYyPGh8amL8/PfS3OC6hZcg4LOiLhj+h+h3eeamTyAE62zjJjC4UMKc +ZEI9ZhQ95BxH9bTS+JxP6UySLnQ= +--------------070807070206050202070908-- + diff --git a/rt/t/data/gnupg/emails/18-signed-encrypted-inline-binary.txt b/rt/t/data/gnupg/emails/18-signed-encrypted-inline-binary.txt new file mode 100644 index 000000000..315ba5898 --- /dev/null +++ b/rt/t/data/gnupg/emails/18-signed-encrypted-inline-binary.txt @@ -0,0 +1,89 @@ +Message-ID: <46BCDC0C.6090400@mit.edu> +Date: Fri, 10 Aug 2007 17:43:40 -0400 +From: Christian Ternus <ternus@mit.edu> +User-Agent: Thunderbird 1.5.0.12 (X11/20070604) +MIME-Version: 1.0 +To: rt-recipient@example.com +Subject: Test Email ID:18 +X-Enigmail-Version: 0.94.2.0 +Content-Type: multipart/mixed; + boundary="------------090909060406090905060708" + +This is a multi-part message in MIME format. +--------------090909060406090905060708 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 8bit + +-----BEGIN PGP MESSAGE----- +Charset: ISO-8859-1 +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAf+KwEEEyzyWahb5iwjSff0D3MfGimfD7AJTEBjjDgtxcC4 +8xKSJsXhZ2feCi3Wx3TYAoFedoiR0DkyNd//B6dE51bpDpoUX1R8M4W0tOKH03ZZ +N3sIBYSnJBF66GdLfjSvyEGD9sdi+OnWTv9DbQ4x9Sq2d3u8PBLu3krwk75hJEH7 +z9Q4rhqzpRU9ymhWpS/QPd+4SvuhlJ0ciUGeYLmOFc1YAUJNWzTtnja/jZptFY7l +8oDXtHTapeJPB9M0WaDf6R60Evkl9DcvGA6FRBMCzWRjqMSdAPGYiceeMiDYcwCq +hJWSzbDKAHDSY2qwlHZ+2y7mIze3x05qcBHYCIOKewgA5mer22WKZIiEP++p4TZF +kJbwFxfCqf7mB/nsRGDWsfThGED4KSr18feba8ychNLbVx83FEtTZ5tei96N6NrK +eS6BdjedTCbvrq7lhIFmC/qPXy/5cjNxr50RBT0sLUEdnY9dhrwLxo69Rqr5qHQT +luYu7/NhQwOV8OlMTpvy/AfUXGiICLa1iBxtVK8UQ9YMLe1GOyCmseeF3UyfTmKN +YfEylY17YRsmYZerulSwsHnEoCipjEHOwzksR5zXDHmnv7cqVq1gV1SnjSXOd1Cd +QuJFdXY5fBdugcrAAJsiB8Iq4t0B7Ai1Lou6x+kKQoigF4i3zhSmH6blZmc99B1r +jYUCDgOxaIPydPr+7xAH/j7bst78EmNmr4RJvQ4A5bss5BJeOdG36MsTkQ4rXTKv +tA+chOccB3irMYqpWpKoDMWRz8VgAu9MVVc8SBUr6XRIhLRyCpuZxlwLA+EJJUk6 +yeKBHZZ8KgQ+PgC6WYMaTcRRLeOWbTxhhIrZmT2EbEEDE1jbeeLNrEu1wmrdMBBb +fhtQTjOHKb7iau3LOGTbbV+F9llnPHrdy2WTp8ozFbsruEqdUG85zQ8X58sy/iC4 +hS9mj+vRs8nanbYABfDHUhoBk8VWUWPlpWTUfzDX0Wuai7LWo5wn7mK0p2i84vMU +IghS5OGRJfrGVK/1giklqUrmPuiz8M2bG9voLoOVnVIH/izycMW2zZh12TD+YySt +D1NP6OPy/5PgYgVZvzTYtOW7Y5EI1eTYoC0cMgjITokq6NKqN9aI7sReVV9P24El +J/LxLvv9Nk7/8Jq6z1CjF0THtFg1mQYTWn52mrYwBr7aPpv/UDDFA0dmIancXwf0 +CHwFMMHFlYSBChy/vwx8+QiE60pLiz6EnajSq8lEQCf6nSFIvmkWn8y3IxmYRgr/ +CMn0BZc+VLgyzIJYG1Ygll10vXDdmjV4Y4f33stWr2Qse5Wp2Pyg/Lsfqw3C7+H1 +1BoxYJma2NmJ6sQu8xDNKPt8dsyOCYEyJqf8KGhi8eslRPuilKOreZ9b/KZh6OE2 +cNTSwA4Buvhbv4626sdq/BJxEi4Cxrhu3dMPCyeyl1540BOSMpThXdKa5pheJTs1 +78UDi87cJBZx8eQ5Gcg2VfwXkolc/dOvIRBEvnuIvA/3DaqH4gxMZ90UCFSzZ9Jm +By83H0GVB0l8UbneSKtbr4wms5qjSUUzT7NTmqOdZwFmxJIbB0hVZtC1ve00WlkG +qSmK2BXyG2bccsjeC/XOKO2WmZm9Gfuvtr6KcOnHFQYj08ZSfDPzJCuY7PK4/egc +TR4wApehk6BzQwbz66uN2PlHIw== +=2ZQG +-----END PGP MESSAGE----- + +--------------090909060406090905060708 +Content-Type: application/octet-stream; + name="favicon.png.pgp" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="favicon.png.pgp" + +hQIOA076g5OuwfIOEAf/fVq/LJs3vrFnybm6thFjpDte1mawn7xw9op6UIFqEFRjkesreTn2 +b4vzYgi/8HUKJj6A/b9OBBobsOuHkKddZ+wRnTnMTc6ZGahJOFdOc3C84hEJYvyEgRqJ9hBh +F6awMfPYHIfN3y6PpeJ8jbtWN2QxYvy9a9dsb7oUGi9FuBC7nI0qIqdxeLwjK4MkVGzDhdJU +N4OCCe3LhYyD+Ev8Ote2NiWFdPnCkFiO6vwYI1cEyzltJXeOXYe/7YCKDb3FcTG9UszG2nDl +jz7hIXF4NtYVQjIxxtRxPfxKgkzboXIoAD1enJpBkNtTdVS3jtdbFvSlypDhG3+mfMlUo/k4 +vwf/RF+30fL1kZjue/Jecd4oh6NcOokKRRScIwxTwittYPVYtR5W+swTRyF+YA5SLfygmmYb +QHDjFX+cOx8u8WzmVgK4szOBgh921p6DWxMdD6aLK2wDT//ZVdRDqNV/0AbApexlDriCP/CH +8BQxHKTCsentMtdVDf66Y/44K6HYceeU8iZT3pvQKXfAmetiMau7G/pN+BvkN2HXFhBPa8id +dHYIpixvjk7Mg3PBzt6mJa4SOSf8vzProFx4UmnkexOQcRxpS0Zoep1mB16oWqm7tXEUjBJx +39BwDg8e9sU4Yrri2WzUkIPU0pG3ub5sxbCTMSxMJnZkRk8ul8s9GMtWYIUCDgOxaIPydPr+ +7xAIAIu4kheO0n6eNJeng1XE/giRvZodwsO27kA00KIW8mi6WGLvehlmFoxntck1r2oCNVvH +Oj94nfWsG2i1zvjQDvMp2Cnp3KTmhTrOss37dFayWVODF/Q9Kx+7WXnh9zCEjQe0eCd6PiKF +fvb4zCU3ANAY8dTmqRcDDB9TK39nafpWHEfzhjClHrQhuQDh+yb4ayXAvUXkNLIYzAvuwvku +9x46MPLHf/4VVQAplymRvsMy/Vj3R9triHoE9tD42EOWgbo525IwLQ590x/AZoZhkCOeffWv +6fq29Z6om8TqFTHFGPb7I88s5ihlpvUgvXkAA7ZYdF6q06pDldWqVOCUm98H/0LZKiiURX1P +30EAe7DpKi0bAUvHj2Qc5GCU+Hf1ASGJmeENFNMwY+eDwYA6wuBf0tN5zitvncUj5SBZjXjV +o8Uz7JJ9/6BuUcfAa4/O2qTIFdctBI9bwzzzuXYF3LbYXfjB4WmoaR0nRh8kTlpgFij7WQlg +l+C1G630ynSm4bVHSmD0J/kBDfNUF7Kyr/riPuqnyqf2wVffbLIJYe/axb4slACZ+F4FhfsZ +0NLYL2+pvGW05sv+k/kEyhSQeQZwmu8X4iZdfFK6kOpkHeS7K/yFX7BYRD9nMnZ5Zwekh8l7 +eVno90OdJ4rVJkp4+c2N3kEPPLgrEVlaut9gybv553PSwSIBeo8TwJX0khartEsmJRh4fuQq +Ni8JhsMDG5Mr8DMvmjhWy5Awp+uQ+neNcnk11wQ+OYDFw4JopRED0m+igEK0i/737ANzEhLN +ssbKnPSivQCBo7Yzv9o3XUx+UU4c3E2bE3MQRGDgDmB1SS2h+wyLDWDoBHGXZ17Hxcb83huJ +HxFt+5e2K8T6hL3Rwghtg1DHt9eVS05v35xomDn1zPR+EWTEEhvWrPEw00CGpsq3ub4vJajJ +Aa+4ClaBivaZVKE4rC87cdLNkiBFbXR3ROvkzoSnrA7/ZUgupDLfagDQKAj91hC1hmF1nRix +SOIYGGeM1IsKm806Ah58IOUWsE5vAzoFqCMB4kYNZLbXU1ccxZVTU6QnpE8DzHoDqOtyN5y3 +2nyRLH9jBwKfECR3YHH3NxRbbI317fhmU5pAvRWeHpRp1yzGrCf21lgx12Ot1EWxHuXrd23p +0O2EiQAcwadItgkfKW+UwAxGsVX+vueqSBYmYViCTFWKGqMXmgXgirR4nLt7L20WqrXBFKBH +ms+7e9LwLHStfAzilr1deHnriNfwz0b9UPAvCV3n6FN3uz2asBWZKjOxlEr2VmaQYLiiFWe0 +UhX685wevk7AOX2LZ7iq7SkVmMLSIMgOVI8vmt2PEw== +--------------090909060406090905060708-- + + diff --git a/rt/t/data/gnupg/emails/19-signed-inline-plain-nested.txt b/rt/t/data/gnupg/emails/19-signed-inline-plain-nested.txt new file mode 100644 index 000000000..eb763bdc1 --- /dev/null +++ b/rt/t/data/gnupg/emails/19-signed-inline-plain-nested.txt @@ -0,0 +1,34 @@ +Received: by anduril (Postfix, from userid 1000) + id 3EDA537F80; Mon, 13 Aug 2007 15:34:17 -0400 (EDT) +Date: Mon, 13 Aug 2007 15:34:17 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:19 +Message-ID: <20070813193417.GD6392@mit.edu> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii; x-action=pgp-signed +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +- -----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +This is a test email with inline nested signature. +ID:19 +- -----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.9 (Darwin) + +iEYEARECAAYFAkmETNkACgkQ0ygDXYSIHxv3ewCgijZQyL5vWIOfk+06XjqTXdrN +VDcAnj13TCHvhas6rMtxcljNNGvPidw6 +=VMc6 +- -----END PGP SIGNATURE----- +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.9 (Darwin) + +iEYEARECAAYFAkmETOIACgkQ0ygDXYSIHxvvvQCfZlRPNjt77jJ7ANxwOpkHkwCY +wsIAn0PzLhCKhIcAm+hk8CpduzYcY0xW +=Xy5t +-----END PGP SIGNATURE----- diff --git a/rt/t/data/gnupg/emails/2-signed-MIME-plain-with-attachment.txt b/rt/t/data/gnupg/emails/2-signed-MIME-plain-with-attachment.txt new file mode 100755 index 000000000..851549c02 --- /dev/null +++ b/rt/t/data/gnupg/emails/2-signed-MIME-plain-with-attachment.txt @@ -0,0 +1,48 @@ +Received: by anduril (Postfix, from userid 1000) + id 74BF637F75; Mon, 13 Aug 2007 15:23:57 -0400 (EDT) +Date: Mon, 13 Aug 2007 15:23:57 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:2 +Message-ID: <20070813192357.GB6392@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/signed; micalg=pgp-sha1; + protocol="application/pgp-signature"; boundary="bKyqfOwhbdpXa4YI" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--bKyqfOwhbdpXa4YI +Content-Type: multipart/mixed; boundary="DKU6Jbt7q3WqK7+M" +Content-Disposition: inline + + +--DKU6Jbt7q3WqK7+M +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline + +This is a test email with a text attachment. +ID:2 + +--DKU6Jbt7q3WqK7+M +Content-Type: text/plain; charset=us-ascii +Content-Disposition: attachment; filename=text-attachment + +This is a test attachment. The magic word is: zanzibar. + +--DKU6Jbt7q3WqK7+M-- + +--bKyqfOwhbdpXa4YI +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: Digital signature +Content-Disposition: inline + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +iD8DBQFGwK/N0ygDXYSIHxsRAlbuAJ4wxUVCNerg6dLm+w7llCj51YYbFACgvNJR +ajbUy9MMkljajl6Of3IlqRA= +=R6Gi +-----END PGP SIGNATURE----- + +--bKyqfOwhbdpXa4YI-- diff --git a/rt/t/data/gnupg/emails/3-signed-MIME-plain-with-binary.txt b/rt/t/data/gnupg/emails/3-signed-MIME-plain-with-binary.txt new file mode 100755 index 000000000..82ef7e9f3 --- /dev/null +++ b/rt/t/data/gnupg/emails/3-signed-MIME-plain-with-binary.txt @@ -0,0 +1,55 @@ +Received: by anduril (Postfix, from userid 1000) + id 9745537F7B; Mon, 13 Aug 2007 15:29:41 -0400 (EDT) +Date: Mon, 13 Aug 2007 15:29:41 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:3 +Message-ID: <20070813192941.GC6392@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/signed; micalg=pgp-sha1; + protocol="application/pgp-signature"; boundary="pY3vCvL1qV+PayAL" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--pY3vCvL1qV+PayAL +Content-Type: multipart/mixed; boundary="at6+YcpfzWZg/htY" +Content-Disposition: inline + + +--at6+YcpfzWZg/htY +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline + +This is a test email with binary attachment and MIME signature. +ID:3 + +--at6+YcpfzWZg/htY +Content-Type: image/png +Content-Disposition: attachment; filename="favicon.png" +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QAAAAAAAD5Q7t/AAAB +BElEQVR42u1WWw6DMAwz0+5FbzbvZuZk2cfUritpea77wVIRIBQ7dhsBdIQkM8AMMJImyW6d +BXweyJ7UAMnUvQFGwHp2bizIJfUTUHZO8j/k1pt8lntvchbdH8ndtqyS+Gj3fyVPAtZAkm3N +ffCyi/chBIQQ3iqs3cQ0TZCERzbhngDocOS4z94wXTCmu2V45LuQW8hsSWpaP8v9sy+2IRZj +ZTP5ububbp8Az4ly5W6QqJ33YwKSkIYbZVy5uNMFsOJGLaLTBMRC8Yy7bmR/OD8TUB00DvkW +AcPSB7FIPoji0AGQBtU4jt+Fh1R6Dcc6B2Znv4HTHTiAJkfXv+ILFy5c8PACgtsiPj7qOgAA +AAAASUVORK5CYII= + +--at6+YcpfzWZg/htY-- + +--pY3vCvL1qV+PayAL +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: Digital signature +Content-Disposition: inline + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +iD8DBQFGwLEl0ygDXYSIHxsRAuYxAKDQeRS40bRiW5jmrwHNsCDN67vu7wCfV0Pd +7T/gCO7TrbuGaJ0BVsJnJsY= +=Pjdg +-----END PGP SIGNATURE----- + +--pY3vCvL1qV+PayAL-- diff --git a/rt/t/data/gnupg/emails/4-signed-inline-plain.txt b/rt/t/data/gnupg/emails/4-signed-inline-plain.txt new file mode 100644 index 000000000..1dcecaedb --- /dev/null +++ b/rt/t/data/gnupg/emails/4-signed-inline-plain.txt @@ -0,0 +1,24 @@ +Received: by anduril (Postfix, from userid 1000) + id 3EDA537F80; Mon, 13 Aug 2007 15:34:17 -0400 (EDT) +Date: Mon, 13 Aug 2007 15:34:17 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:4 +Message-ID: <20070813193417.GD6392@mit.edu> +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii; x-action=pgp-signed +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +This is a test email with inline signature. +ID:4 +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +iD8DBQFGwLI50ygDXYSIHxsRAp40AJ9ErYdLH2SVRXtgRtx7n/FVFOmKDwCgl/0T +BeRSaF4Xbi8uGhVIkmU+YCs= +=e4u6 +-----END PGP SIGNATURE----- diff --git a/rt/t/data/gnupg/emails/5-signed-inline-with-attachment.txt b/rt/t/data/gnupg/emails/5-signed-inline-with-attachment.txt new file mode 100644 index 000000000..638f0fda5 --- /dev/null +++ b/rt/t/data/gnupg/emails/5-signed-inline-with-attachment.txt @@ -0,0 +1,48 @@ +Message-ID: <46BCDA81.3030308@mit.edu> +Date: Fri, 10 Aug 2007 17:37:05 -0400 +From: Christian Ternus <ternus@mit.edu> +User-Agent: Thunderbird 1.5.0.12 (X11/20070604) +MIME-Version: 1.0 +To: rt-recipient@example.com +Subject: Test Email ID:5 +X-Enigmail-Version: 0.94.2.0 +Content-Type: multipart/mixed; + boundary="------------010302000403000103080306" + +This is a multi-part message in MIME format. +--------------010302000403000103080306 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit + +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +This is a test email with a text attachment and inline signature. +ID:5 +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +iD8DBQFGvNqA0ygDXYSIHxsRAuCHAKC0HnduLWqihY5wzGYDFGbFtA4chwCgr6+t +mQo4oXIqu+kIZ0ExWyiUENs= +=3ABp +-----END PGP SIGNATURE----- + +--------------010302000403000103080306 +Content-Type: text/plain; + name="text-attachment" +Content-Transfer-Encoding: 7bit +Content-Disposition: inline; + filename="text-attachment" + +This is a test attachment. The magic word is: zanzibar. + +--------------010302000403000103080306 +Content-Type: application/octet-stream; + name="text-attachment.sig" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="text-attachment.sig" + +iD8DBQBGvNqB0ygDXYSIHxsRAmkbAJ0esGNEDMr01u45ZHIIKZpCFSE8tgCfXBedq0Yu5mnZ +zOZyASZYUIf9wSE= +--------------010302000403000103080306-- diff --git a/rt/t/data/gnupg/emails/6-signed-inline-with-binary.txt b/rt/t/data/gnupg/emails/6-signed-inline-with-binary.txt new file mode 100644 index 000000000..2c725aa58 --- /dev/null +++ b/rt/t/data/gnupg/emails/6-signed-inline-with-binary.txt @@ -0,0 +1,55 @@ +Message-ID: <46BCDAE6.4090803@mit.edu> +Date: Fri, 10 Aug 2007 17:38:46 -0400 +From: Christian Ternus <ternus@mit.edu> +User-Agent: Thunderbird 1.5.0.12 (X11/20070604) +MIME-Version: 1.0 +To: rt-recipient@example.com +Subject: Test Email ID:6 +X-Enigmail-Version: 0.94.2.0 +Content-Type: multipart/signed; micalg=pgp-sha1; + protocol="application/pgp-signature"; + boundary="------------enigAEEA002E4229CA8E5445ED73" + +This is an OpenPGP/MIME signed message (RFC 2440 and 3156) +--------------enigAEEA002E4229CA8E5445ED73 +Content-Type: multipart/mixed; + boundary="------------000104020205010403010301" + +This is a multi-part message in MIME format. +--------------000104020205010403010301 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable + +This is a email signed inline with a binary attachment. +ID:6 + +--------------000104020205010403010301 +Content-Type: image/png; + name="favicon.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="favicon.png" + +iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QAAAAAAAD5Q7t/AAAB +BElEQVR42u1WWw6DMAwz0+5FbzbvZuZk2cfUritpea77wVIRIBQ7dhsBdIQkM8AMMJImyW6d +BXweyJ7UAMnUvQFGwHp2bizIJfUTUHZO8j/k1pt8lntvchbdH8ndtqyS+Gj3fyVPAtZAkm3N +ffCyi/chBIQQ3iqs3cQ0TZCERzbhngDocOS4z94wXTCmu2V45LuQW8hsSWpaP8v9sy+2IRZj +ZTP5ububbp8Az4ly5W6QqJ33YwKSkIYbZVy5uNMFsOJGLaLTBMRC8Yy7bmR/OD8TUB00DvkW +AcPSB7FIPoji0AGQBtU4jt+Fh1R6Dcc6B2Znv4HTHTiAJkfXv+ILFy5c8PACgtsiPj7qOgAA +AAAASUVORK5CYII= +--------------000104020205010403010301-- + +--------------enigAEEA002E4229CA8E5445ED73 +Content-Type: application/pgp-signature; name="signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="signature.asc" + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +iD8DBQFGvNrm0ygDXYSIHxsRAvwSAKC4d3U6SjfhYpUHu2V/vXtgxGFa1QCfeK6p +dyDDlvlqP9Ns4EExvHXfHuY= +=sX3V +-----END PGP SIGNATURE----- + +--------------enigAEEA002E4229CA8E5445ED73-- diff --git a/rt/t/data/gnupg/emails/7-encrypted-MIME-plain.txt b/rt/t/data/gnupg/emails/7-encrypted-MIME-plain.txt new file mode 100644 index 000000000..11e3b7cda --- /dev/null +++ b/rt/t/data/gnupg/emails/7-encrypted-MIME-plain.txt @@ -0,0 +1,47 @@ +Received: by anduril (Postfix, from userid 1000) + id CEA9137F51; Fri, 10 Aug 2007 15:27:49 -0400 (EDT) +Date: Fri, 10 Aug 2007 15:27:49 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:7 +Message-ID: <20070810192749.GA5572@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; + boundary="YiEDa0DAkWCtVeE4" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--YiEDa0DAkWCtVeE4 +Content-Type: application/pgp-encrypted +Content-Disposition: attachment + +Version: 1 + +--YiEDa0DAkWCtVeE4 +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAf9FtBMrkLWUKK5BtwuUsYXV9Mbe/YkkmK61MuysAtLcX/M +DiXPzngjL62Dr9l88R3imf2kPmY36yx5WNeXUrFFVmFPaeZrHEJiMNvPGVVQCqRK +uar2vsYRK9th4msZnn0hBYnA8+8kZ8rWefWHpszOcJ1YZpyyEMLf8Vnstyf0Pebp +Wxixr99+mn3MVH38CrhoErI6CMiCFJgPAl5wtGWd0lT3+657dLJCsNI0cT3AY/JC +IJwWD2sdOXOzDo7tdC3l7/YuGsXvd4jGu4A8PdoBMOgPx/N4KT3+uPhp2sRD3PMg +LU59613xT8/FOYxQSib9hGqNZPqXS3ryC3ZvY4Sp8AgApCMocKsN7vm8N+6Yh7Nc +Jjy/kuf8tjuTTs32Yk0ACU3y2SFXKSBZo6cVXgJhUvmG2Dq4O/A8mtP0cjqeBFqp ++vZOb7xhtxxTE6HWWThvx5qxcwjpijzDMS9uzfGaHwLvewdGVLODCup06MJmeAmj +N1WEZqc/cqFZvZ9omCpcvTGoELpOzcUY1MxAq1IVkMzAk7dPIHYuyPSFQK6Y8IPl +xsfSMcq9gjth8qautnriB0ohwkUebGnxgM4CjGjnSmLmUFXkndUOXKbM7R7E3QdL ++6TKMr2pvLl8U3OJrCiyyPVyVi3in4TYi4uegXJl05CAEjEXRf5RFhaWRnn66EYN +WdLAAQHpkESfESVUaWvJwI+JB+LMBoKZjWgvIQ7AQKqLAvIsAqs9PKM4mYOMaawl +en9XNRkW0dSGUxjW4K8u7fLS/xzWrZeCrafEkvCowVv/nR+Wxm296bxX+7z2R52/ +j+J0zms1fRdVxEs+rOI6JuXg4xWxUdLTav7cqvQ5c/izM+jU4yWEa3y0fHma6Jeh +o4+1NerQby8Yzxszh9XVfkbYnQilhP8qCVxYe4HGjKlNi5v/xOgCznCKsqkGYMPU +S32K6lg= +=xeKr +-----END PGP MESSAGE----- + +--YiEDa0DAkWCtVeE4-- diff --git a/rt/t/data/gnupg/emails/8-encrypted-MIME-with-attachment.txt b/rt/t/data/gnupg/emails/8-encrypted-MIME-with-attachment.txt new file mode 100644 index 000000000..3781a62e1 --- /dev/null +++ b/rt/t/data/gnupg/emails/8-encrypted-MIME-with-attachment.txt @@ -0,0 +1,49 @@ +Received: by anduril (Postfix, from userid 1000) + id C962637F57; Fri, 10 Aug 2007 15:39:14 -0400 (EDT) +Date: Fri, 10 Aug 2007 15:39:14 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:8 +Message-ID: <20070810193914.GC5572@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; + boundary="FFoLq8A0u+X9iRU8" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--FFoLq8A0u+X9iRU8 +Content-Type: application/pgp-encrypted +Content-Disposition: attachment + +Version: 1 + +--FFoLq8A0u+X9iRU8 +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAgA4dZa3t55kB5pY4Q7+h4thTJVZ+wV2QxI5wZFRvMs7yw3 +MEIWqHusoe7R2zeXLxaT0gMSQj3osO+GrGJRDk4qCGjcjSneFmYKkELx8KRML59N +X7/HeX7v8nSGpyqM5foaky38uAGxj2xBpswfvsY8qdQ37a4mNH2atESnkxYLomZb +4vTRTSE1XtvhaG7oHaXxfmeNln0JezfbfpDGTktZJOupEX99j6/bvloimXyBT6SZ +3wbcP/EYoqC8DQOQTtFTAisWwveTVp6FDFvi5L2BCJ2NzYZSDOBNeXrzf1AJ04dN +9lTzbtejdE/AgwDclDnZYZByiGLM43X6nTk7c9gH1AgAlrxxvO1yz4sZchGM2rrn +yzNJpcvcgbJLTt54gqie/BdQHSnvlCBLZzplx2xV9HIwJB7Kf29Ka2gg2p6mjyID +YUd1451K1hDUmBHgya8jb6g2c6bBxiusUODlGfmv+c+kq/yG0tKws5d0/IGanL+Z +h7XkcEYq3its0acCbcKizgzLSmxSqu6NK7rnDvioUOHHKG8uC9fk2JetdTdYOTBn +AWYDxa3D+kRvOAiOWGMqtvOWAC1BZJ8hssBpesuG1+sucTh7W4ZROtQfEbqC8W51 +s6e17x1YUqE8QIs/aUYkUkX2JqObKsZPJAiVYCQkqJceIk/lT4rXNOfUwl5dAynk +WNLAZQGKuVghSfSoRyKbFMRDimigisWN2JUudaV0Ld6E/5iO7EP7XhjqkzTlwsaN +wui2n0Omxye2dNVMKz3q76fVp79XbznaI1ckeVjiiDmkiaQOp1Au/Sx2Bj0/wolJ +OXnp27oqc9THy3RANLRXIQuRaYOyoxUIjVvhbOVfM3U7MlcAW3jT2kMTI7H83HWc +ezxKfqM7nJUKkIVmRY4J/6X4uo5c8DdIxLeG+ioj+3I7BRkLfIAPYDGaeB5+BD1k +EPcVGd3u1EW7D8f5CRARnU8aaC8DCPk5YYN6wM9JY9n0FEgIkwoniTpqHLs/UMa8 +DWGer1UCGp4qElmHvWHVV8b82nw6Ta2BhWKjA9pphf96KOTt3y4jqxfU4GbrOYDJ +786/cggItsc0 +=qJrM +-----END PGP MESSAGE----- + +--FFoLq8A0u+X9iRU8-- diff --git a/rt/t/data/gnupg/emails/9-encrypted-MIME-with-binary.txt b/rt/t/data/gnupg/emails/9-encrypted-MIME-with-binary.txt new file mode 100644 index 000000000..cafc88077 --- /dev/null +++ b/rt/t/data/gnupg/emails/9-encrypted-MIME-with-binary.txt @@ -0,0 +1,57 @@ +Received: by anduril (Postfix, from userid 1000) + id 215C737F5D; Fri, 10 Aug 2007 15:46:27 -0400 (EDT) +Date: Fri, 10 Aug 2007 15:46:27 -0400 +To: rt-recipient@example.com +Subject: Test Email ID:9 +Message-ID: <20070810194627.GA5815@mit.edu> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; + boundary="GID0FwUMdk1T2AWN" +Content-Disposition: inline +User-Agent: Mutt/1.5.13 (2006-08-11) +From: ternus@mit.edu (Christian Ternus) + + +--GID0FwUMdk1T2AWN +Content-Type: application/pgp-encrypted +Content-Disposition: attachment + +Version: 1 + +--GID0FwUMdk1T2AWN +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.6 (GNU/Linux) + +hQIOA076g5OuwfIOEAgAqwVRwpeOIRMvrgswuYagJfk57HvSvrAg7tVzXHaHW5LI +zJeWpKg0PVwmJUYTG9cZN0gkfpr33SULruvniZRSX650E/vv9H03P1dVYhBbcP01 +AyfPoaySinipoqG5OK7aTJVEnPZqqsCKgzFbqEg+FdF3YjNVa1etfmKV/JvoXTxR +il6pOqv+xUJ66GpPUoP7mCZJ/cJEF1zz/EbZoYcS9MgxoP42bOIYBhyqIv3fUh7X +fuDpup5xnJwM3FRjkNQPlqKhgGHCgk04OGgLUw8wzRwYQ5jSyij45J/UcGoNWePi +eVTZignbYIfrlwa5GH6776U9QA9ei7PFP3RqO2CB2Af6AiIdkeN9MfPFfiQ/ASwR +/zePg+OOmKC6locoUHWTEuBCZhgty7XarfPZufBHlA3Z3f369Pxz9nMV07KkO22t +thhltpsxan2pFZL4oPfwA9OdDRGeWIISn4jGc+foNfKTmERY7EZv9ruDC0lxaSvs +HEWJC38sJ3xGdlS439qaddSbm0Lft8JbNwi6FEpThE4abBdITu+BxbkoFrjIy6aC +UJgyJ3YtPTpU8JuT49Ocn+51YPXZTSc/ePnvlzSqRXHeJrOKp5Oyoa9001242MRM +mpaC1lw/sfGIIAXv5t2B569RXDsZ2jIvPjFIleIdyKOG17m8qsP82nWKbN2N96am +p9LpAb5gqid4Hw1PjjjCB0A9w7G9orQeEQuF+2V9MGWAEWOHoVbGWTRjfh4uXASz +4PlazbNDIZZB09ACaDfxG3Y3/TI2X0sl9KJNRALFtHm4+DNbWVeWe+aKrG9ZY9Os +xjD/UQFFQrchpNPD7xV98WWEg4+s8pkk9Mwv+Q/gNf2xBXJXDOhIm2LlY0VlhETq +fk4YS3EUS78Ti4V99w/L9PTnoaZY/8kkI6NNjb+bqldhd9AQHXEkpV5Gh/PrsgFn +FzBZxEwcY8wKP2yKMTp+A3rsjHwM4OChjtkDShf4KDwGS5D+E4o1RJVcM3jfh2SG +RRKge5ewErBmIDIqeU8Wpj5cuyqS77CIB3aplSKLqUu2bC2EiLFZxs2UTg+cp5WK +WFzt+YmH6/p0y9eyaAGOleSh3dnIZHv2BDtzF9x5fHFesjaVp3jpmMsHQ1ol53TR +8N4fUuO140oE0Gnci61EXzLjkGCSBhzZqy/+K8PR0d1hGv+vlwfadpzsPfX8WcxZ +DBuI1E7OcqVaXemR90/C2AbLB8qGuwQ+wTIHPqXlm8s0+6wZ4YUcUoa/F+2VLSxN +VWGji1yioKrSaBjlBBUpHfoC/Q/0hLk2FgIrAZFIPvhHl9nF3Vq/HqlxtGZQ7gnL +TjQtRuI+oU7IhJRR1VZbhr8xn1pirueiiwJUPub6w4XfMGyvcvlIOVpUImW5Hab7 +kg20iwrcJNLCcPJsR5iEbhJBBHLsViMtfdRbstlEV8I4wTY2tNAkfMtUtWdUhwOA +fdx/UmHjbNZFSS6cpsGHL8+QJ2Bo1urkHIz6z1w4f1vTL31NAPeyvZ4kNstSqkNt +6fjdrt8mBRVGQfqRQsK2b/R61ErJoFULboeqOT/ed/Dufu2Wxf/wuuRvG77S8xoq +OiDT8nhyHh7ljjdHZZ2uLro8gg== +=BIke +-----END PGP MESSAGE----- + +--GID0FwUMdk1T2AWN-- diff --git a/rt/t/data/gnupg/emails/README b/rt/t/data/gnupg/emails/README new file mode 100644 index 000000000..634f53794 --- /dev/null +++ b/rt/t/data/gnupg/emails/README @@ -0,0 +1,28 @@ +Set of emails signed and/or encrypted using real MUAs(mutt, thunderbird). + +All messages have subejct 'Test Email ID:\d+'. +ID matches number in name of the corresponding file. +Top most entity of a message as well contains 'ID:\d+' text. + +Emails may have either text plain or binary attachment. +Text of plain attachment is +"This is a test attachment. The magic word is: zanzibar." + +Content of binary attachment is RT's favicon.png. + +Encrypted messages encrypted for rt-recipient@example.com. + +Messages signed using rt-test@example.com key. + +Name of a file may contain the following parts separated by dash: +* signed +* encrypted +* MIME - RFC format +* inline - inline format +* with-attachment - text plain attachment +* with-binary - binary attachment + +In total 18 emails using all possible combinations. +18 = 3 (signed/encrypted/both) * 2 (MIME/inline) + * 3 (no/text/binary) + diff --git a/rt/t/data/gnupg/keyrings/pubring.gpg b/rt/t/data/gnupg/keyrings/pubring.gpg Binary files differnew file mode 100644 index 000000000..f993bf2db --- /dev/null +++ b/rt/t/data/gnupg/keyrings/pubring.gpg diff --git a/rt/t/data/gnupg/keyrings/secring.gpg b/rt/t/data/gnupg/keyrings/secring.gpg Binary files differnew file mode 100644 index 000000000..eda64ae94 --- /dev/null +++ b/rt/t/data/gnupg/keyrings/secring.gpg diff --git a/rt/t/data/gnupg/keyrings/signed_old_style_with_attachment.eml b/rt/t/data/gnupg/keyrings/signed_old_style_with_attachment.eml new file mode 100644 index 000000000..0bae0d4af --- /dev/null +++ b/rt/t/data/gnupg/keyrings/signed_old_style_with_attachment.eml @@ -0,0 +1,48 @@ +Message-ID: <45A10003.1090705@bestpractical.com> +Date: Sun, 07 Jan 2007 17:13:23 +0300 +From: "Ruslan U. Zakirov" <ruz@bestpractical.com> +User-Agent: Thunderbird 1.5.0.9 (X11/20061221) +MIME-Version: 1.0 +To: rt@example.com +Subject: test +X-Enigmail-Version: 0.94.1.0 +Content-Type: multipart/mixed; + boundary="------------030903040907010006050500" + +This is a multi-part message in MIME format. +--------------030903040907010006050500 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +inline + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.6 (GNU/Linux) +Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org + +iD8DBQFFoQADtaRiGUNF96URAmHSAKCDdVnRJ2gb1idhE1ZXEg1JARalsQCgkaU8 +74cnNxVyp/0XwBA73qzkvx0= +=UmxP +-----END PGP SIGNATURE----- + +--------------030903040907010006050500 +Content-Type: text/plain; + name="test.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="test.txt" + +YXR0YWNobWVudAo= +--------------030903040907010006050500 +Content-Type: application/pgp-signature; + name="test.txt.sig" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="test.txt.sig" + +iD8DBQBFoQADtaRiGUNF96URAqmBAJ42zyr06nK6R4dNpZD5067DNDgjRwCgkR+SKgz7OdAq +p11D7PQGCR1Wuvg= +--------------030903040907010006050500-- diff --git a/rt/t/data/gnupg/keyrings/trustdb.gpg b/rt/t/data/gnupg/keyrings/trustdb.gpg Binary files differnew file mode 100644 index 000000000..9f2ae63a2 --- /dev/null +++ b/rt/t/data/gnupg/keyrings/trustdb.gpg diff --git a/rt/t/data/gnupg/keys/general-at-example.com.2.public.key b/rt/t/data/gnupg/keys/general-at-example.com.2.public.key new file mode 100644 index 000000000..04fbe9666 --- /dev/null +++ b/rt/t/data/gnupg/keys/general-at-example.com.2.public.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.7 (Darwin) + +mQGiBEbgjJQRBACx8uMHcl9JlKCUU5yh0rBw636MXhKr7cH/+zBTeUVQyfs+J4fo +HlI3fwJzZOxwXKTwmXsHSKxomeBJxPpFCnwHvn7xTYh6Wtis1Rm2Vr4lsw4gxrFj +bpk1ISkHo4tO3dPTNx6Jhz+gYzoVUyfclz/byUmEbe2HJkBCQNmC9/lcuwCg477S +siOdObqTKtQhDqXzFfOHKmsEAKVB8RImtLAO/HPY5+rxiVkfKjsmZovi0PfioGDI +3o1jcSwq/RwWPZTNB0vnlEx1aD3zedUn1T7ZPnKoRttIv0xYlg9wYhX/xvOA9mKy +G5aXSypiJiNwJSQfcChlyVHX3R88pURIzPiwWv2OBOvRagE8Bmgz0DFa0/AQfmvI +vpUmA/4qZVzOJ5hC6fVP+ESKEC6fStvHAEZM3sQK5AnZsuUls3+tgfkD9T6ei5YF +MXDLMGz3thue1M0QEu3IL7cLoTMalWQpjpyDuqXS8UAsd5cG2eP2iA6Uf2VoKQ11 +w6Q1FyMwTC9B0F8JijwdLF77ERSCtIG+TOA1EtH9HfTV5+BfHbQfZ2VuZXJhbCAy +IDxnZW5lcmFsQGV4YW1wbGUuY29tPohgBBMRAgAgBQJG4IyUAhsDBgsJCAcDAgQV +AggDBBYCAwECHgECF4AACgkQ32UfoGMsT1De7QCgu2Gws7EHllWWhJC9mHgDHC45 +Z8YAn2mBLO7ZBi5lptavTfQlR05RvTSKuQINBEbgjJQQCAD55mgtF494BvjhumUE +Zu15W9QL99772EntAHnGd6tx6r2GlqWmIHrze/jguIdyXqVGEVWAosDAu08vF1EC +DDJoCz/tkyjhXOT4MBKWL0Gpetk61Pi2Qv0GsxQSYG+FwKjTk+pE4qxnT932n3U8 +7KxUtgIxzH7Y/oFxgb0LGQxgokSPuq/E+jtglYxey9fzeFWjr2T77g5oWu9PXPCF +jg+km3f9rQxxRgDuM4bPr5tWjhLTpNB0xKZMbx6YUVUP/DyK/DhqaMZ5Qt1vZ0QI +/COSPy9Kq76wR1WW6E0SDFU49xAbtMm2MZgDh5uj/lFp95yDGmBSZzwxY0qy/BbN +c4HDAAMFCADbiPty42xieGtEtQdvjqejEsE15Zhna+zf/4Fch/Ee9laOmtW9Nekg +2ltUMHw+Nc5VseWIKtFqlnOZtcutO6yYgVH+GJ287gnXw3gK8zEcbQ2RUxYEdiuw +ORuMqowmVlOCFxgPW6Vv5Em8E3kb1dEMx+Ec/SVmeXpOcGV5kS9PS9veO3ruoXqx +6l3Gq3BU3xvDhJ3FRRlsUYtGOBuJl0/oXk/1TUjAuQinsxM+Nh4VljeI3eiT2Ygb +ypdXjH6zmTDy7PlEy5RDYdFOsKW1sZXKWoY6y2TU4IO9JBWehS8Lhn3pMTv65FME +6Ow9W5+hLTBYo9E/kUZXWvPlJgGA1439iEkEGBECAAkFAkbgjJQCGwwACgkQ32Uf +oGMsT1BE+gCeOni/yGg3+IIPpIoTCVsf8jUcJXgAn3nsf/TTUli9XyftXRiJbua9 +CM0m +=bGLz +-----END PGP PUBLIC KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/general-at-example.com.2.secret.key b/rt/t/data/gnupg/keys/general-at-example.com.2.secret.key new file mode 100644 index 000000000..6a8510e58 --- /dev/null +++ b/rt/t/data/gnupg/keys/general-at-example.com.2.secret.key @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.7 (Darwin) + +lQHhBEbgjJQRBACx8uMHcl9JlKCUU5yh0rBw636MXhKr7cH/+zBTeUVQyfs+J4fo +HlI3fwJzZOxwXKTwmXsHSKxomeBJxPpFCnwHvn7xTYh6Wtis1Rm2Vr4lsw4gxrFj +bpk1ISkHo4tO3dPTNx6Jhz+gYzoVUyfclz/byUmEbe2HJkBCQNmC9/lcuwCg477S +siOdObqTKtQhDqXzFfOHKmsEAKVB8RImtLAO/HPY5+rxiVkfKjsmZovi0PfioGDI +3o1jcSwq/RwWPZTNB0vnlEx1aD3zedUn1T7ZPnKoRttIv0xYlg9wYhX/xvOA9mKy +G5aXSypiJiNwJSQfcChlyVHX3R88pURIzPiwWv2OBOvRagE8Bmgz0DFa0/AQfmvI +vpUmA/4qZVzOJ5hC6fVP+ESKEC6fStvHAEZM3sQK5AnZsuUls3+tgfkD9T6ei5YF +MXDLMGz3thue1M0QEu3IL7cLoTMalWQpjpyDuqXS8UAsd5cG2eP2iA6Uf2VoKQ11 +w6Q1FyMwTC9B0F8JijwdLF77ERSCtIG+TOA1EtH9HfTV5+BfHf4DAwJaviEcavOA +YWAyNs+6FnvZb5JMpdZWxKrmidVFbPUx7sHk2vEW0UTzcnqnlxmoJ/tKeWwiNl1j +ztCVFbQfZ2VuZXJhbCAyIDxnZW5lcmFsQGV4YW1wbGUuY29tPohgBBMRAgAgBQJG +4IyUAhsDBgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQ32UfoGMsT1De7QCgu2Gw +s7EHllWWhJC9mHgDHC45Z8YAn2mBLO7ZBi5lptavTfQlR05RvTSKnQJjBEbgjJQQ +CAD55mgtF494BvjhumUEZu15W9QL99772EntAHnGd6tx6r2GlqWmIHrze/jguIdy +XqVGEVWAosDAu08vF1ECDDJoCz/tkyjhXOT4MBKWL0Gpetk61Pi2Qv0GsxQSYG+F +wKjTk+pE4qxnT932n3U87KxUtgIxzH7Y/oFxgb0LGQxgokSPuq/E+jtglYxey9fz +eFWjr2T77g5oWu9PXPCFjg+km3f9rQxxRgDuM4bPr5tWjhLTpNB0xKZMbx6YUVUP +/DyK/DhqaMZ5Qt1vZ0QI/COSPy9Kq76wR1WW6E0SDFU49xAbtMm2MZgDh5uj/lFp +95yDGmBSZzwxY0qy/BbNc4HDAAMFCADbiPty42xieGtEtQdvjqejEsE15Zhna+zf +/4Fch/Ee9laOmtW9Nekg2ltUMHw+Nc5VseWIKtFqlnOZtcutO6yYgVH+GJ287gnX +w3gK8zEcbQ2RUxYEdiuwORuMqowmVlOCFxgPW6Vv5Em8E3kb1dEMx+Ec/SVmeXpO +cGV5kS9PS9veO3ruoXqx6l3Gq3BU3xvDhJ3FRRlsUYtGOBuJl0/oXk/1TUjAuQin +sxM+Nh4VljeI3eiT2YgbypdXjH6zmTDy7PlEy5RDYdFOsKW1sZXKWoY6y2TU4IO9 +JBWehS8Lhn3pMTv65FME6Ow9W5+hLTBYo9E/kUZXWvPlJgGA1439/gMDAlq+IRxq +84BhYLhZWMP6gRz3MuuJr0YX10x+bx3/96Wkh505MRMLqefr0J8WgzjIJS1aIUqA +nusWttcVsQZS3ZHIZk/tp5dq54CICn09Rl+UNySISQQYEQIACQUCRuCMlAIbDAAK +CRDfZR+gYyxPUET6AJwNpkgxKEdjvIbdB7Y+IPgA1wyt+ACdGc0py11j1RLa2gn0 +1+nPnBIS7R8= +=oSXs +-----END PGP PRIVATE KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/general-at-example.com.public.key b/rt/t/data/gnupg/keys/general-at-example.com.public.key new file mode 100644 index 000000000..c274c8df9 --- /dev/null +++ b/rt/t/data/gnupg/keys/general-at-example.com.public.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.7 (Darwin) + +mQGiBEYrdhQRBACpTwAjSJchxV9rgWJj/4GUe92xZ2wWHVrkv7cELO5GD1ie8wtA +nh57oXfcFhuSmtLTyT/C1Mbzo/tz4Sigf33bZlEMXusp0bLsSz1S/5mslBGRdApJ +Dz5jETEcakpzWsHA5sHfv8HLn+o8WUDtlGZf+Edi0DqUKSiLRkjWMAdcJwCg7cN1 +IIhhFFAf9Lr3Ny7ngJDwn/sEAJoZVUmhBHo9TipR9lZY1si5U0hA8Yn4XghLp4z9 +0rm8dAgLSZwFI2/zoU5u9qjW0UAo8Sp2SO9F03wQpfUGnpQtea/HVNuwiZVU42bB +E5gn5EIYrHYT8X7cd+ZpWVGYu2117uoJtRHwnfuh857ocs7M7xeo7IQUZArqeHOG +i3hABACV9mqnZoPyCOtaBogdXtDlEbqDvYclONJTsSKAfPsNRjJi8lzvJL9ZhtS8 +YKIUvxFu+XX0UVXWoNnzte8Ip/0hwupJu9jIcBJpI9dVEK3H2tWr+NElzML/uch+ +VO7UUmk2H/hF8+a3wXkdEN45FnJCyqC0Kk59OcY3bJIrI56SZrQdZ2VuZXJhbCA8 +Z2VuZXJhbEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRit2FAIbAwYLCQgHAwIEFQII +AwQWAgMBAh4BAheAAAoJEJ+mYsBt4i/CNxwAoMEYt4mJlH9rCoqc8kkaE2qJslG+ +AJ9plkUk5LWv7ncWcrUpMxVo5J3Eh7kCDQRGK3YwEAgAqxHhoyoViGJsImMKG2XQ +wxHIBJc21zaLwG8MAn6xNXIZRDrjlgy0ItteGB0Hs1VqibE70fwZ9q5O/Ev32M/s +fxtEgJMyfZzOVPMxY0AZu7sUdsfDiTqV/0FwKMjGM1aV2ulqaVZLJr4hpdxjgynf +TUqtkJjYtUMAwMcsee/ORnyuSBGcaG3SjWyOyrI+oDm4e2QZ/jGmINP4b3PHJu9C +JXq3dR/vJGlYxr9uApaXHjVksa5+uPA6iqjyCoRo/Sv+V3SANSOYJYI9tnKFHuLM +YAqwOufWNnvHjb2zS0hCAJJ6p0CMcBhmaNieXcOgmt8MIJe/b5W4ItfgS/2D+yI/ +iwADBQf+ISTkh2pY2QExXtGmhjO1LSxB/sO5n0zJOkQvf/I5aOgWxXDP8dYcc9mp +2sV5okB+S4Dol/ppGaxas+kisdwTNja12Er21Lrm6CGfyqp214EKZcM44KmIb1HA +utLlujBJsWgf6LDggH4s785GKzwtA9NHfiWeWkXWGXrGFsFK6OWDcdSH0GcHus2F +9FHLyOh5jpkd4Xczx5lEAn1wvUhQtRn9bBU7FLGUind352vbKegeFjUvhGCLp+YF +IxSB2OXeVdzUbMjiUM0zJu3VNDsq9IP5EBvV3cwixuOF/XzYiDIPqezDDU9P5H2R +d1IQBgMIPl9WXAFRkCIJUnsmvQOINIhJBBgRAgAJBQJGK3YwAhsMAAoJEJ+mYsBt +4i/CEuwAn1QHO3umF9MHhpWQIBaacJUfBboqAKCYPpflpFvDNn0ioLA7Dw5qVSix +5A== +=CF0O +-----END PGP PUBLIC KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/general-at-example.com.secret.key b/rt/t/data/gnupg/keys/general-at-example.com.secret.key new file mode 100644 index 000000000..36f9ba3e3 --- /dev/null +++ b/rt/t/data/gnupg/keys/general-at-example.com.secret.key @@ -0,0 +1,31 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.7 (Darwin) + +lQG7BEYrdhQRBACpTwAjSJchxV9rgWJj/4GUe92xZ2wWHVrkv7cELO5GD1ie8wtA +nh57oXfcFhuSmtLTyT/C1Mbzo/tz4Sigf33bZlEMXusp0bLsSz1S/5mslBGRdApJ +Dz5jETEcakpzWsHA5sHfv8HLn+o8WUDtlGZf+Edi0DqUKSiLRkjWMAdcJwCg7cN1 +IIhhFFAf9Lr3Ny7ngJDwn/sEAJoZVUmhBHo9TipR9lZY1si5U0hA8Yn4XghLp4z9 +0rm8dAgLSZwFI2/zoU5u9qjW0UAo8Sp2SO9F03wQpfUGnpQtea/HVNuwiZVU42bB +E5gn5EIYrHYT8X7cd+ZpWVGYu2117uoJtRHwnfuh857ocs7M7xeo7IQUZArqeHOG +i3hABACV9mqnZoPyCOtaBogdXtDlEbqDvYclONJTsSKAfPsNRjJi8lzvJL9ZhtS8 +YKIUvxFu+XX0UVXWoNnzte8Ip/0hwupJu9jIcBJpI9dVEK3H2tWr+NElzML/uch+ +VO7UUmk2H/hF8+a3wXkdEN45FnJCyqC0Kk59OcY3bJIrI56SZgAAn2a5NRggeMB1 +vXJiR6c1NjYO2+hHCRC0HWdlbmVyYWwgPGdlbmVyYWxAZXhhbXBsZS5jb20+iGAE +ExECACAFAkYrdhQCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCfpmLAbeIv +wjccAKDBGLeJiZR/awqKnPJJGhNqibJRvgCfaZZFJOS1r+53FnK1KTMVaOSdxIed +Aj0ERit2MBAIAKsR4aMqFYhibCJjChtl0MMRyASXNtc2i8BvDAJ+sTVyGUQ645YM +tCLbXhgdB7NVaomxO9H8GfauTvxL99jP7H8bRICTMn2czlTzMWNAGbu7FHbHw4k6 +lf9BcCjIxjNWldrpamlWSya+IaXcY4Mp301KrZCY2LVDAMDHLHnvzkZ8rkgRnGht +0o1sjsqyPqA5uHtkGf4xpiDT+G9zxybvQiV6t3Uf7yRpWMa/bgKWlx41ZLGufrjw +Ooqo8gqEaP0r/ld0gDUjmCWCPbZyhR7izGAKsDrn1jZ7x429s0tIQgCSeqdAjHAY +ZmjYnl3DoJrfDCCXv2+VuCLX4Ev9g/siP4sAAwUH/iEk5IdqWNkBMV7RpoYztS0s +Qf7DuZ9MyTpEL3/yOWjoFsVwz/HWHHPZqdrFeaJAfkuA6Jf6aRmsWrPpIrHcEzY2 +tdhK9tS65ughn8qqdteBCmXDOOCpiG9RwLrS5bowSbFoH+iw4IB+LO/ORis8LQPT +R34lnlpF1hl6xhbBSujlg3HUh9BnB7rNhfRRy8joeY6ZHeF3M8eZRAJ9cL1IULUZ +/WwVOxSxlIp3d+dr2ynoHhY1L4Rgi6fmBSMUgdjl3lXc1GzI4lDNMybt1TQ7KvSD ++RAb1d3MIsbjhf182IgyD6nsww1PT+R9kXdSEAYDCD5fVlwBUZAiCVJ7Jr0DiDQA +AVQKdEMomLc49M9nDgc3mQkmkS1Ce0xI+BBEf0LhdiUYRU9mclN7HkCj3fl8EUCI +SQQYEQIACQUCRit2MAIbDAAKCRCfpmLAbeIvwhLsAKCoZAIqS3Bp2WndQqZJvHBS +u4f0VACgslT4IpJF1BdbMvA+oyvYVgv3g/c= +=linj +-----END PGP PRIVATE KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/recipient-at-example.com.public.key b/rt/t/data/gnupg/keys/recipient-at-example.com.public.key new file mode 100644 index 000000000..3d3032ffa --- /dev/null +++ b/rt/t/data/gnupg/keys/recipient-at-example.com.public.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.7 (Darwin) + +mQGiBEWOOBARBAClfdK23heqW39sO3K0p+KtZkDxWhgpjjRfMSWQWwY++eDFhDr5 +BGG3zxsB2R8MOaHVKetuDjWjmfDi/LvDR8br8+eaLt94F4PBcLa+vVjboYyjvTFs +2t3leyXjNd+mBTZmUcY6WLM0E+biIJdlDhVOv95n8VwvT10R0J/mGO7hTwCg20NC +vJJ/fpWzPItFfKHnKg3gO3MD/RhG6CxmsZs+1bdzY07UwABQG8NhoR5Veqg2+uBr +xiYjemtC+8fAtthEojJ334BE7qDuXEO7eq1R+JOtEj/Hx8gtgWQfDNZlgLA4NUSc +aU3PthtXD4CnY5MsTrxjpD+bjTde6ziEJ3RHPQQSq2S1fKqo5Bf6H7GYWueRiSGS +cNK2A/4/WAYFQbj9Jm7zgvPrLRRnk7RP3A4+ABaWEtGMRbpCaFEHd63gjYEH7P3i +/O6y9kXsYr3SkDbhk6h1Cx8+4fjpZHAd4XbgabZhp5u7Nq3m/TIzQiXMYXrZGleB +CGoQrERbM9mavEgOHGZEwO1I/JKSHdmAg/adTRAbG+AxZK79u7QhVGVzdCBVc2Vy +IDxyZWNpcGllbnRAZXhhbXBsZS5jb20+iGAEExECACAFAkWOOBACGwMGCwkIBwMC +BBUCCAMEFgIDAQIeAQIXgAAKCRBIVe2Ik+ud58m2AKCAO8NDxsU8m6ahhfwrPaQG +O7MjQwCgtDn5aYx4G93pv5UKpGzkHKh2T1G5Ag0ERY44FBAIALAc3V9FsEK3pxdc +ILYVjV85rrL58+hGcsgeNqF2TIgneJ3Dwi0zA82K1gchfvBT7Ab+A7WGi6E0rXnO +s/rMoDV+5Qoac4M9hW1qb1seBt+0au72npIMSqF5V/4nZr4L21g6vE4/cgrLd3BK +bA1hCDdIGeqG3Ljiy++RGnhIkiY+FGpNYYAI9bkXltC9BYtl5DBvMrpTqlDUfSzq +rz3zZYaD30FUGbTTqfISU1RC4sY7aEmgFvB6vvia/s9XyldngPFwuTELCAG/JkFd +ZvodkwlTdv/vIY0SFkHJSjmT2a797wuhxuaC17eWrQhfF0sxsZhJ1Ac3osrGQQvr +8dZCd2cAAwUH/icHkxwmYKa7UPQZYexD4QsGS+rq7TbYzbSWcxz2l5J11/pHdD6m +tdGbtEn5mQPjBnOI3GYwvtqbzG4WO5y5qssYoW22ZL9ov67QpJyKJILNLInWwiqx +oRgB3Kgk5J5vDNw7CZLxrEvQNKE1gTEqfmQGUAiXipJ2VXbTWenPN6fDv5vdKesF +dnDmk+jfjbL0/G8jUwt6vnQXMVZnIuxTMxs+4tTQfK1qh5iMdaC3wy15pg2wZoky +OzjVEJywIQmqAA8lcvD0S/l4+JM6Epfx8gd9IjOVULUd2/TiPSvGM/gpJdop3bUc +UmlWeF59Xx9uZPo7twDubKB6grMWOZwGgM2ISQQYEQIACQUCRY44FAIbDAAKCRBI +Ve2Ik+ud5+EVAJ0TZHopcAmCANc26gPWWDfTaZovmQCcC5jl3o/yXvdaAhfbqKMr +cts0+T8= +=wrrX +-----END PGP PUBLIC KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/recipient-at-example.com.secret.key b/rt/t/data/gnupg/keys/recipient-at-example.com.secret.key new file mode 100644 index 000000000..620a4327e --- /dev/null +++ b/rt/t/data/gnupg/keys/recipient-at-example.com.secret.key @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.7 (Darwin) + +lQHhBEWOOBARBAClfdK23heqW39sO3K0p+KtZkDxWhgpjjRfMSWQWwY++eDFhDr5 +BGG3zxsB2R8MOaHVKetuDjWjmfDi/LvDR8br8+eaLt94F4PBcLa+vVjboYyjvTFs +2t3leyXjNd+mBTZmUcY6WLM0E+biIJdlDhVOv95n8VwvT10R0J/mGO7hTwCg20NC +vJJ/fpWzPItFfKHnKg3gO3MD/RhG6CxmsZs+1bdzY07UwABQG8NhoR5Veqg2+uBr +xiYjemtC+8fAtthEojJ334BE7qDuXEO7eq1R+JOtEj/Hx8gtgWQfDNZlgLA4NUSc +aU3PthtXD4CnY5MsTrxjpD+bjTde6ziEJ3RHPQQSq2S1fKqo5Bf6H7GYWueRiSGS +cNK2A/4/WAYFQbj9Jm7zgvPrLRRnk7RP3A4+ABaWEtGMRbpCaFEHd63gjYEH7P3i +/O6y9kXsYr3SkDbhk6h1Cx8+4fjpZHAd4XbgabZhp5u7Nq3m/TIzQiXMYXrZGleB +CGoQrERbM9mavEgOHGZEwO1I/JKSHdmAg/adTRAbG+AxZK79u/4DAwKKjpV+74Fq +wGCggxQNXbWP1fy2s8C5f3k/jsyGcPyDv2qcVoo9f1cLdb6Alu3f6kSAGWpUink9 +dHErTbQhVGVzdCBVc2VyIDxyZWNpcGllbnRAZXhhbXBsZS5jb20+iGAEExECACAF +AkWOOBACGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRBIVe2Ik+ud58m2AKCA +O8NDxsU8m6ahhfwrPaQGO7MjQwCgtDn5aYx4G93pv5UKpGzkHKh2T1GdAmMERY44 +FBAIALAc3V9FsEK3pxdcILYVjV85rrL58+hGcsgeNqF2TIgneJ3Dwi0zA82K1gch +fvBT7Ab+A7WGi6E0rXnOs/rMoDV+5Qoac4M9hW1qb1seBt+0au72npIMSqF5V/4n +Zr4L21g6vE4/cgrLd3BKbA1hCDdIGeqG3Ljiy++RGnhIkiY+FGpNYYAI9bkXltC9 +BYtl5DBvMrpTqlDUfSzqrz3zZYaD30FUGbTTqfISU1RC4sY7aEmgFvB6vvia/s9X +yldngPFwuTELCAG/JkFdZvodkwlTdv/vIY0SFkHJSjmT2a797wuhxuaC17eWrQhf +F0sxsZhJ1Ac3osrGQQvr8dZCd2cAAwUH/icHkxwmYKa7UPQZYexD4QsGS+rq7TbY +zbSWcxz2l5J11/pHdD6mtdGbtEn5mQPjBnOI3GYwvtqbzG4WO5y5qssYoW22ZL9o +v67QpJyKJILNLInWwiqxoRgB3Kgk5J5vDNw7CZLxrEvQNKE1gTEqfmQGUAiXipJ2 +VXbTWenPN6fDv5vdKesFdnDmk+jfjbL0/G8jUwt6vnQXMVZnIuxTMxs+4tTQfK1q +h5iMdaC3wy15pg2wZokyOzjVEJywIQmqAA8lcvD0S/l4+JM6Epfx8gd9IjOVULUd +2/TiPSvGM/gpJdop3bUcUmlWeF59Xx9uZPo7twDubKB6grMWOZwGgM3+AwMCio6V +fu+BasBgNB3AutvANzxcWBPZSJFs3uN0m4ii+g7eu8fYaiIJO7GFk1WD5UORHs9w +0C4JIqUj10yUzvnHBZ6DdkV2aZE9FZljLLpRs+iiwYhJBBgRAgAJBQJFjjgUAhsM +AAoJEEhV7YiT653n4RUAoJd1DuGANpd7VGGwNfoMCCpAk/4TAKCrbh6NpoJr0Ab4 +1toDA5lM/EYYPw== +=5O4y +-----END PGP PRIVATE KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/rt-recipient-at-example.com.public.key b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.public.key new file mode 100644 index 000000000..9952c6f99 --- /dev/null +++ b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.public.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.7 (GNU/Linux) + +mQGiBEa8u6QRBADCqPh8w3cO51hPVb1Sttqq5UhCeB5t2dAL8aVEDkpPfV7LItDi +pN4VqHo2zbGE8q2bCoqW06Ogn0R4xsxEeD9Jq9/k3dHReFL2gbA5F/el1PKXVxG8 +62BnjLkDub8yCdWsg0QDJ6ah7LC7vukTMlJj+3HhoXWEqBrTBKjtFkNIrwCg/LtU +CEyj+z/cl6NQGZUw2A6+5DUD/2DfcLeSir7xrlcidqO4BxtxdWkEBDAnmARKrqaw +zSATIK11+HO3Gteovfa08J1XXU2+IFqi2Ssyaqss1kteJE8DmOAcllSXqmCfOmPm +xoW4gXOQfEv6tkTvF9JST1OZRj5w+ecyxn0282XrzKcxNeLjc+JcLfzPmmuhw4lA +s/nJA/4tBqT0V7QiwaznBo8Bh7N3sz75x0vgSdZLUA0e2VzHKh9mAfK/FeVS1mcJ +04iHWvxOGMqEfXnpxUrogME7f/TWNBVfT4M2JW0sHLvaiJhTtIhn+Q67awQ1f0qG +mGQLIo9OAWZnIfBZ8e2tBwJ3ajiSZ2LIPWFv4Q1hKxOclODpf7QmUlQgVXNlciBC +b2IgPHJ0LXJlY2lwaWVudEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRry7pAIbAwYL +CQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEM4wxB7CJ0omPPUAoL3A7jiEZX6xSyXE +duAtnmMplqHrAKDB8mrNols8/ni0VOv0QletwEwbVrkCDQRGvLuvEAgA/qDyDeDP +FrDhh757tpgvJp2CmIx8fyv+i9nLEBVCZjtkLqgrcvtNh8l+xu3y8vjGB6+ToPvG +ZE3FRxyLWNPGIlq1pQSREC9faEDWDrN7yA8miaikLIlfMnGwwzb5bEXWsmXzctTv +DgxTCufDj8T66TKv+cCqc9T956XY6q49Z/p6yZDiY7LZ0N6GkHSoT8o6ZCOvl87n +IjwKR8AXDWBxL5+SeenNkZ8e30pSVDJTOe4u6W/MKK3RBD0FKYr+DOMh5BQtE7yT +QEhzmDTPfGe9m52FV8FbSLpimMnIFM2hGRf6jynoR10s0tk2DVADXDycwNYarRYG +AxV6XafLCPDv4wADBQgAtpM7zhVch/NsL56aIG0QZmSaKCdk6UPsJua91eLEHJFo +zOzethsAWED5KHD5ThsYBKPGq+mFz7QQtw8/DBmcajtBxMv2fvVOE7SrWfeHyMVl +RgidJc3O6HlPPnA/v8lQhsYTxpUddYqB4lC0ktpncxCzX/VNr62YkmrpJx2Yvyd0 +L/lK5fiko65gQC1v/XQ/QI9kpGbOFXFnEgQXmFcDTX4kzTgpJ3cOBrM9GAO/hcwH +82eC0j8fYw8mLYR8yQG0jsXJKCvHxTgkOh0nSkLaeLoq1maLp+NbJKCqgpsmeV4n +QmEJE4Ye7I/L077BtJLv1tk0G0Jh3F2WeSzEvB7cS4hJBBgRAgAJBQJGvLuvAhsM +AAoJEM4wxB7CJ0om/FcAn2tCGofP7IPmw6VxGBZNPHal4sIBAJ9UCgpOaGtX2fRl ++vvcvfcuIys27g== +=mo7N +-----END PGP PUBLIC KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/rt-recipient-at-example.com.secret.key b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.secret.key new file mode 100644 index 000000000..853801fd6 --- /dev/null +++ b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.secret.key @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.7 (GNU/Linux) + +lQHhBEa8u6QRBADCqPh8w3cO51hPVb1Sttqq5UhCeB5t2dAL8aVEDkpPfV7LItDi +pN4VqHo2zbGE8q2bCoqW06Ogn0R4xsxEeD9Jq9/k3dHReFL2gbA5F/el1PKXVxG8 +62BnjLkDub8yCdWsg0QDJ6ah7LC7vukTMlJj+3HhoXWEqBrTBKjtFkNIrwCg/LtU +CEyj+z/cl6NQGZUw2A6+5DUD/2DfcLeSir7xrlcidqO4BxtxdWkEBDAnmARKrqaw +zSATIK11+HO3Gteovfa08J1XXU2+IFqi2Ssyaqss1kteJE8DmOAcllSXqmCfOmPm +xoW4gXOQfEv6tkTvF9JST1OZRj5w+ecyxn0282XrzKcxNeLjc+JcLfzPmmuhw4lA +s/nJA/4tBqT0V7QiwaznBo8Bh7N3sz75x0vgSdZLUA0e2VzHKh9mAfK/FeVS1mcJ +04iHWvxOGMqEfXnpxUrogME7f/TWNBVfT4M2JW0sHLvaiJhTtIhn+Q67awQ1f0qG +mGQLIo9OAWZnIfBZ8e2tBwJ3ajiSZ2LIPWFv4Q1hKxOclODpf/4DAwIc+jyN96r4 +cWCKJH3rJKMiam7fzkjUhawkIXBXWlau1oZeQKvQCxCpj02aFks9bSTK9wseazjU +JRF6D7QmUlQgVXNlciBCb2IgPHJ0LXJlY2lwaWVudEBleGFtcGxlLmNvbT6IYAQT +EQIAIAUCRry7pAIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEM4wxB7CJ0om +PPUAoL3A7jiEZX6xSyXEduAtnmMplqHrAKDB8mrNols8/ni0VOv0QletwEwbVp0C +YwRGvLuvEAgA/qDyDeDPFrDhh757tpgvJp2CmIx8fyv+i9nLEBVCZjtkLqgrcvtN +h8l+xu3y8vjGB6+ToPvGZE3FRxyLWNPGIlq1pQSREC9faEDWDrN7yA8miaikLIlf +MnGwwzb5bEXWsmXzctTvDgxTCufDj8T66TKv+cCqc9T956XY6q49Z/p6yZDiY7LZ +0N6GkHSoT8o6ZCOvl87nIjwKR8AXDWBxL5+SeenNkZ8e30pSVDJTOe4u6W/MKK3R +BD0FKYr+DOMh5BQtE7yTQEhzmDTPfGe9m52FV8FbSLpimMnIFM2hGRf6jynoR10s +0tk2DVADXDycwNYarRYGAxV6XafLCPDv4wADBQgAtpM7zhVch/NsL56aIG0QZmSa +KCdk6UPsJua91eLEHJFozOzethsAWED5KHD5ThsYBKPGq+mFz7QQtw8/DBmcajtB +xMv2fvVOE7SrWfeHyMVlRgidJc3O6HlPPnA/v8lQhsYTxpUddYqB4lC0ktpncxCz +X/VNr62YkmrpJx2Yvyd0L/lK5fiko65gQC1v/XQ/QI9kpGbOFXFnEgQXmFcDTX4k +zTgpJ3cOBrM9GAO/hcwH82eC0j8fYw8mLYR8yQG0jsXJKCvHxTgkOh0nSkLaeLoq +1maLp+NbJKCqgpsmeV4nQmEJE4Ye7I/L077BtJLv1tk0G0Jh3F2WeSzEvB7cS/4D +AwIc+jyN96r4cWBgxgN9v8Z6ySQrlQfJWkB3LDcYVugGb3Ht6vIMaFMnW9KFwOgd +/nuf5uqyuy2/jQ0SZT1fUC3skhxT1BXKewfmmlnzoLJu3rz6iEkEGBECAAkFAka8 +u68CGwwACgkQzjDEHsInSib8VwCeLdgy5axSbYZ8Ez42Kcj8Ku2Q4ZUAnjJ92jFc +fC7XWcM/6AX5IyU0jtEW +=DS2U +-----END PGP PRIVATE KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.2.public.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.public.key new file mode 100644 index 000000000..d63d4dd48 --- /dev/null +++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.public.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.7 (GNU/Linux) + +mQGiBEbl4D0RBADu+s2KTTSMl2/aK3Jkhy9ZTBMFOOCPeleidjV8z7RVGEwTjcby +B0DbPFC/eK0ot9m/F9CojE6QHK0hqjCKPfARptjG6C/Iqxql0DaRWdo4UYTgT6WW +hhoKK5DUN57Eu0essy1qTyzcXVIRsQdfkn2ldRKC1XSXnKAiL0vODLtL7wCgoDgj +tDtOHdi0vlSHvRhPD1F9P3sEAMvSaiEMN/3AlAWQLqrg0rQRr4dpRZqahoffBIeX +OZGzDSrWtIshMQLLA0HmkphPtRe/y74GBWfpwr6Hs6yl5tP2PSXsAAl+W92st2Vp +lKJWsLZtvW39nS5cwmv0Etz6j0F9tn7Ah4+x89egzIg9GwU14cS2GNqxYsK3+YMY +jSXzA/4zEDOQkrRuSEm9JNG5JCFKexAvjLzhYQQRCOI1PrX3iAMzbYFFIgTpr26h +sPfOb5SMy2OGeECXGd0rxF4+rMCbp0jrQ8B18CWuho7HJK97WuT6NFoaPZCh/pYK +OQkKGnJCUQNSm5u4uWY6yNs/+U4kvYJvIGw7F7uNWuXcpfREabQmUlQgVGVzdCB0 +aGUgc2FtZSA8cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRuXgPQIbAwYL +CQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEMeYWRqoMdv7v1oAnR9bV/4nbzizkEAm +691AuqGLFyryAJ9WjLWviRXuiEKMR6LMIn9HC0POarkCDQRG5eA9EAgAoexx9eOo +hJpX0VJ8gNVSlFsLLq60ugiWzfwzrGkL2v4o5QoCwj6XFhK/xtDmqhWu3USBVtqW +vrMuq2VCWWPiezZ/8Zl8NY7GjxDyLgeuEotfBkS8qFLQH7TGqNFLoJOIi3UjNFX8 +cRx29CSQyc3jj88HiC1InuMMwDXf8ukpkYNG0n7E3lZ3dWOadCXY2+kAxJ5qGV0U +WmEBoux9TU8hFAq4DtRmf2x/Mt3k+e5ZXpbnbKGxAAImMV8WR0SGAd5OcgeE+rB3 +ziAtD8YbM+WDxbihloje7YN0hVSFSTsGvypIC/jPSNTVgkJdG7N0NepGh/T3wbAV +eDebMYDmUQYLiwADBQgAmrAkyor1V/M21ERoZdoNFMlbdxuuQ5QhBIkkiygn2dq1 +d7CaOF1Hi5s3o6DsD9ER2YENlPYgPtVPH/agGuITHHugGKitj9sRM/QCGMKf/IKa +zxcTQfcHmOUMXrM6GWzPZiiTrIBpOZyRDyFN1x0y9G9z7lVt507AkyF3aS2da6yc +l2knSX3vPtFkHCUVlvKz6yXGE4xLm8/ACH/zPDjuQ2X/Yz+FfpJAeNihz6/J88Ga +O7sgU1jx3zdRaH5hZXJRlyO7AW1KdOtI8xZi3SBNXKBnfCbUUSBbxa2spXst0Eep +ijEDtsDhK+imdFjX8ul/JE0XhJooRrcdevEbohMn2IhJBBgRAgAJBQJG5eA9AhsM +AAoJEMeYWRqoMdv7OI4AnR5PPGNNdhVSTle3B9cV2Vy11gEjAJ0QHe4iFf7wfASF +jybMsNKYwFb5FQ== +=XFiw +-----END PGP PUBLIC KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.2.secret.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.secret.key new file mode 100644 index 000000000..83207f100 --- /dev/null +++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.secret.key @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.7 (GNU/Linux) + +lQHhBEbl4D0RBADu+s2KTTSMl2/aK3Jkhy9ZTBMFOOCPeleidjV8z7RVGEwTjcby +B0DbPFC/eK0ot9m/F9CojE6QHK0hqjCKPfARptjG6C/Iqxql0DaRWdo4UYTgT6WW +hhoKK5DUN57Eu0essy1qTyzcXVIRsQdfkn2ldRKC1XSXnKAiL0vODLtL7wCgoDgj +tDtOHdi0vlSHvRhPD1F9P3sEAMvSaiEMN/3AlAWQLqrg0rQRr4dpRZqahoffBIeX +OZGzDSrWtIshMQLLA0HmkphPtRe/y74GBWfpwr6Hs6yl5tP2PSXsAAl+W92st2Vp +lKJWsLZtvW39nS5cwmv0Etz6j0F9tn7Ah4+x89egzIg9GwU14cS2GNqxYsK3+YMY +jSXzA/4zEDOQkrRuSEm9JNG5JCFKexAvjLzhYQQRCOI1PrX3iAMzbYFFIgTpr26h +sPfOb5SMy2OGeECXGd0rxF4+rMCbp0jrQ8B18CWuho7HJK97WuT6NFoaPZCh/pYK +OQkKGnJCUQNSm5u4uWY6yNs/+U4kvYJvIGw7F7uNWuXcpfREaf4DAwJXViZBji9w +O2Bhhip5V1QOR7FbE8SLAJVVPoX4Lv8iXOMm4jaXAqTPkvWmWZDiUKfDf6Dv5wT1 +aj8N8LQmUlQgVGVzdCB0aGUgc2FtZSA8cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQT +EQIAIAUCRuXgPQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEMeYWRqoMdv7 +v1oAnR9bV/4nbzizkEAm691AuqGLFyryAJ9WjLWviRXuiEKMR6LMIn9HC0POap0C +YwRG5eA9EAgAoexx9eOohJpX0VJ8gNVSlFsLLq60ugiWzfwzrGkL2v4o5QoCwj6X +FhK/xtDmqhWu3USBVtqWvrMuq2VCWWPiezZ/8Zl8NY7GjxDyLgeuEotfBkS8qFLQ +H7TGqNFLoJOIi3UjNFX8cRx29CSQyc3jj88HiC1InuMMwDXf8ukpkYNG0n7E3lZ3 +dWOadCXY2+kAxJ5qGV0UWmEBoux9TU8hFAq4DtRmf2x/Mt3k+e5ZXpbnbKGxAAIm +MV8WR0SGAd5OcgeE+rB3ziAtD8YbM+WDxbihloje7YN0hVSFSTsGvypIC/jPSNTV +gkJdG7N0NepGh/T3wbAVeDebMYDmUQYLiwADBQgAmrAkyor1V/M21ERoZdoNFMlb +dxuuQ5QhBIkkiygn2dq1d7CaOF1Hi5s3o6DsD9ER2YENlPYgPtVPH/agGuITHHug +GKitj9sRM/QCGMKf/IKazxcTQfcHmOUMXrM6GWzPZiiTrIBpOZyRDyFN1x0y9G9z +7lVt507AkyF3aS2da6ycl2knSX3vPtFkHCUVlvKz6yXGE4xLm8/ACH/zPDjuQ2X/ +Yz+FfpJAeNihz6/J88GaO7sgU1jx3zdRaH5hZXJRlyO7AW1KdOtI8xZi3SBNXKBn +fCbUUSBbxa2spXst0EepijEDtsDhK+imdFjX8ul/JE0XhJooRrcdevEbohMn2P4D +AwJXViZBji9wO2DcFJ5JiQd3X934uG+AeTFIWQG75qOWS3j8BpRn50H73tUn9dxB +Z+V9tI7sMFW3cQTnt6c+FSNJBTUAfLTExQO0BqPSrVwVmKImiEkEGBECAAkFAkbl +4D0CGwwACgkQx5hZGqgx2/s4jgCfeukl8vl9mMuariu08MsuywQ77y4An1cIwl1x +997LwJrR5WF/WoGvPQ61 +=E3hO +-----END PGP PRIVATE KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.public.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.public.key new file mode 100644 index 000000000..db359a6eb --- /dev/null +++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.public.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.7 (GNU/Linux) + +mQGiBEa4o04RBADc+2sFcnuTTaqcKRTmSBQKdXvumT4GoATk194UYMghwprrNb1/ +flXQRk9zLkc0YENFHLMoRUmXKEF+WFxzXrZgHJS096tGn+Ud2FXQbSL47Vl3EHng +c+jSvvVaZRcEySaCyQrsDR7gWlQtCbxbe96Y2x9jX3Zbih9UYnRvWBeczwCg4tgz +EOmScnWiwUdyZNQsvXDqvKUD/REf0WjWDaykQvXYZ0aTpc/WMBsDS16nl8GNz5eD +lCB/JJHKh5QDu89p0557AbVDSi5LCOYAM+v4oi8k5zgiO/7HJptirDkZ27Ichyes +kzhu3Xr9rPLawie/o4FCfncNLbOAEE4EjEGDGRlyowAaXlW7DWT+TLbxY0qL0uHy +AQPGA/9AmYHBJQqHTfQ4/QXdCnp+UwYs+rhPh7YHymBLn8Saa14heE9SZcYfSerL +FAE7KKeBx96+RplgsiaqfWrliUwrV3KnnJICMyqWmn2OyMYiV9iFWqAHFTCsitS2 +q1COv5/Lg1a+XkAwEfoIuLrAXT8buIxXs/BhLc1PD1t9My8srbQdUlQgVGVzdCA8 +cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRrijTgIbAwYLCQgHAwIEFQII +AwQWAgMBAh4BAheAAAoJENMoA12EiB8bMZoAoIxLNWQ9d9+W4ImPMpUmjLl9ttxW +AJ9ELlhkfdhPukRe508p5fZqKUfl/rkCDQRGuKNhEAgAjSKFedFcU9RjLmSEGo7E +4qMQWOtiNooW4NtRsKC2cbJXnGJUOT+GBzGCxjBZt89T6MVsOy7DoAzs+xWKA5Cf +gFEX5xZWM1c6EA4f7LhC1hawtGQkMQIyHzEy9b7NPEcMlkdOebjjhZ4Ob8svGily +Q9jN6zpR2c36i0sLaZ5gORIHJ9DOX1k5lUzEhkogEYoYof48VQwHt/5xUURli2kL +Daqi+X2+6j/vNp96EQ3sbFifmNejWNaDyyrlyGUvx9g/Eh5wMRospmFA/oE1kSws +tKiBxAPs11OJGBRre2Q6QVW2ULAhxZOFgkCq0DNb8TMnhJOY4jhOP57rrvpMyu9q +4wADBQgAhRl4aiej9lX+YpZUcyhBkqIB/cDwYemmtIWzo6mVWuDuVcyLl//sJsBi +pwJF6O5nr0ZC5CT+GRgjBmh9rQjv/UtWBldJ7og/HfuSMG6xIfljO2FxKjabDhGa +iKzgTk75LnPqfx0FeRNbN78dPy4hV/iIvHPANuyUlmbBsx9hSGqMc78FIDwwfZtB +im5XUJbpHsahu4/8agQLBu+PFK+5CIVWskrYVL1R66nPHCzsfYcOv+1CYCsuFldG +tFNPCegGxE7T7CFs7m7aYeSdgycNaR9wuBZeV17JnOJ2z/mtsR+8p4vZZzSMcTkj +f1j7UCgiKS/ioTD03pU/OZEy9+l1SYhJBBgRAgAJBQJGuKNhAhsMAAoJENMoA12E +iB8bOvUAoMbxSLj4OsaYEQ1FxOfsXPRmSPVWAJ91PvFOwLkhsm88kjPAIc8oymjd +7A== +=ABM8 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.secret.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.secret.key new file mode 100644 index 000000000..e76fe8428 --- /dev/null +++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.secret.key @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.7 (Darwin) + +lQHhBEa4o04RBADc+2sFcnuTTaqcKRTmSBQKdXvumT4GoATk194UYMghwprrNb1/ +flXQRk9zLkc0YENFHLMoRUmXKEF+WFxzXrZgHJS096tGn+Ud2FXQbSL47Vl3EHng +c+jSvvVaZRcEySaCyQrsDR7gWlQtCbxbe96Y2x9jX3Zbih9UYnRvWBeczwCg4tgz +EOmScnWiwUdyZNQsvXDqvKUD/REf0WjWDaykQvXYZ0aTpc/WMBsDS16nl8GNz5eD +lCB/JJHKh5QDu89p0557AbVDSi5LCOYAM+v4oi8k5zgiO/7HJptirDkZ27Ichyes +kzhu3Xr9rPLawie/o4FCfncNLbOAEE4EjEGDGRlyowAaXlW7DWT+TLbxY0qL0uHy +AQPGA/9AmYHBJQqHTfQ4/QXdCnp+UwYs+rhPh7YHymBLn8Saa14heE9SZcYfSerL +FAE7KKeBx96+RplgsiaqfWrliUwrV3KnnJICMyqWmn2OyMYiV9iFWqAHFTCsitS2 +q1COv5/Lg1a+XkAwEfoIuLrAXT8buIxXs/BhLc1PD1t9My8srf4DAwKhnHYPLWS2 +9GBnewzagq2czolDuKHrmtb1Eiv4mb4S8X6HhSn4gQSUJ59mVsn7L1TwK7yWJgK0 ++Ix66LQdUlQgVGVzdCA8cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRrij +TgIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJENMoA12EiB8bMZoAoIxLNWQ9 +d9+W4ImPMpUmjLl9ttxWAJ9ELlhkfdhPukRe508p5fZqKUfl/p0CYwRGuKNhEAgA +jSKFedFcU9RjLmSEGo7E4qMQWOtiNooW4NtRsKC2cbJXnGJUOT+GBzGCxjBZt89T +6MVsOy7DoAzs+xWKA5CfgFEX5xZWM1c6EA4f7LhC1hawtGQkMQIyHzEy9b7NPEcM +lkdOebjjhZ4Ob8svGilyQ9jN6zpR2c36i0sLaZ5gORIHJ9DOX1k5lUzEhkogEYoY +of48VQwHt/5xUURli2kLDaqi+X2+6j/vNp96EQ3sbFifmNejWNaDyyrlyGUvx9g/ +Eh5wMRospmFA/oE1kSwstKiBxAPs11OJGBRre2Q6QVW2ULAhxZOFgkCq0DNb8TMn +hJOY4jhOP57rrvpMyu9q4wADBQgAhRl4aiej9lX+YpZUcyhBkqIB/cDwYemmtIWz +o6mVWuDuVcyLl//sJsBipwJF6O5nr0ZC5CT+GRgjBmh9rQjv/UtWBldJ7og/HfuS +MG6xIfljO2FxKjabDhGaiKzgTk75LnPqfx0FeRNbN78dPy4hV/iIvHPANuyUlmbB +sx9hSGqMc78FIDwwfZtBim5XUJbpHsahu4/8agQLBu+PFK+5CIVWskrYVL1R66nP +HCzsfYcOv+1CYCsuFldGtFNPCegGxE7T7CFs7m7aYeSdgycNaR9wuBZeV17JnOJ2 +z/mtsR+8p4vZZzSMcTkjf1j7UCgiKS/ioTD03pU/OZEy9+l1Sf4DAwKhnHYPLWS2 +9GBUcgG+SE35K+ynz0mpxRRx4kbgN9Ap6oxzhDYVGRbfDpVxE8hgJuc7zJ27pmPr +VwmdzDROCDy1W9bwjfrV8yhln81npumXxndSiEkEGBECAAkFAka4o2ECGwwACgkQ +0ygDXYSIHxs69QCgxvFIuPg6xpgRDUXE5+xc9GZI9VYAn3U+8U7AuSGybzySM8Ah +zyjKaN3s +=cv/+ +-----END PGP PRIVATE KEY BLOCK----- diff --git a/rt/t/delegation/cleanup_stalled.t b/rt/t/delegation/cleanup_stalled.t new file mode 100644 index 000000000..f108eccf2 --- /dev/null +++ b/rt/t/delegation/cleanup_stalled.t @@ -0,0 +1,458 @@ +#!/usr/bin/perl -w +use strict; +use warnings; + +# Regression test suite for http://rt3.fsck.com/Ticket/Display.html?id=6184 +# and related corner cases related to cleanup of delegated ACEs when +# the delegator loses the right to delegate. This causes complexities +# due to the fact that multiple ACEs can grant different delegation +# rights to a principal, and because DelegateRights and SuperUser can +# themselves be delegated. + +# The case where the "parent" delegated ACE is removed is handled in +# the embedded regression tests in lib/RT/ACE_Overlay.pm . + + +use RT; + +use RT::Test tests => 98; + +my ($u1, $u2, $g1, $g2, $g3, $pg1, $pg2, $ace, @groups, @users, @principals); +@groups = (\$g1, \$g2, \$g3, \$pg1, \$pg2); +@users = (\$u1, \$u2); +@principals = (@groups, @users); + +my($ret, $msg); + +$u1 = RT::User->new($RT::SystemUser); +( $ret, $msg ) = $u1->LoadOrCreateByEmail('delegtest1@example.com'); +ok( $ret, "Load / Create test user 1: $msg" ); +$u1->SetPrivileged(1); +$u2 = RT::User->new($RT::SystemUser); +( $ret, $msg ) = $u2->LoadOrCreateByEmail('delegtest2@example.com'); +ok( $ret, "Load / Create test user 2: $msg" ); +$u2->SetPrivileged(1); +$g1 = RT::Group->new($RT::SystemUser); +( $ret, $msg) = $g1->LoadUserDefinedGroup('dg1'); +unless ($ret) { + ( $ret, $msg ) = $g1->CreateUserDefinedGroup( Name => 'dg1' ); +} +ok( $ret, "Load / Create test group 1: $msg" ); +$g2 = RT::Group->new($RT::SystemUser); +( $ret, $msg) = $g2->LoadUserDefinedGroup('dg2'); +unless ($ret) { + ( $ret, $msg ) = $g2->CreateUserDefinedGroup( Name => 'dg2' ); +} +ok( $ret, "Load / Create test group 2: $msg" ); +$g3 = RT::Group->new($RT::SystemUser); +( $ret, $msg) = $g3->LoadUserDefinedGroup('dg3'); +unless ($ret) { + ( $ret, $msg ) = $g3->CreateUserDefinedGroup( Name => 'dg3' ); +} +ok( $ret, "Load / Create test group 3: $msg" ); +$pg1 = RT::Group->new($RT::SystemUser); +( $ret, $msg ) = $pg1->LoadPersonalGroup( Name => 'dpg1', + User => $u1->PrincipalId ); +unless ($ret) { + ( $ret, $msg ) = $pg1->CreatePersonalGroup( Name => 'dpg1', + PrincipalId => $u1->PrincipalId ); +} +ok( $ret, "Load / Create test personal group 1: $msg" ); +$pg2 = RT::Group->new($RT::SystemUser); +( $ret, $msg ) = $pg2->LoadPersonalGroup( Name => 'dpg2', + User => $u2->PrincipalId ); +unless ($ret) { + ( $ret, $msg ) = $pg2->CreatePersonalGroup( Name => 'dpg2', + PrincipalId => $u2->PrincipalId ); +} +ok( $ret, "Load / Create test personal group 2: $msg" ); + + + +# Basic case: u has global DelegateRights through g1 and ShowConfigTab +# through g2; then u is removed from g1. + +clear_acls_and_groups(); + +( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights' ); +ok( $ret, "Grant DelegateRights to g1: $msg" ); +( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'ShowConfigTab' ); +ok( $ret, "Grant ShowConfigTab to g2: $msg" ); +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); +ok( + $u1->PrincipalObj->HasRight( + Right => 'DelegateRights', + Object => $RT::System + ), + "test user 1 has DelegateRights after joining g1" +); +( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g2: $msg" ); +ok( + $u1->PrincipalObj->HasRight( + Right => 'ShowConfigTab', + Object => $RT::System + ), + "test user 1 has ShowConfigTab after joining g2" +); + +$ace = RT::ACE->new($u1); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'ShowConfigTab', + Object => $RT::System, + PrincipalType => 'Group', + PrincipalId => $g2->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg1: $msg" ); +ok( + $pg1->PrincipalObj->HasRight( + Right => 'ShowConfigTab', + Object => $RT::System + ), + "Test personal group 1 has ShowConfigTab right after delegation" +); + +( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId ); +ok( $ret, "Delete test user 1 from g1: $msg" ); +ok( + not( + $pg1->PrincipalObj->HasRight( + Right => 'ShowConfigTab', + Object => $RT::System + ) + ), + "Test personal group 1 lacks ShowConfigTab right after user removed from g1" +); + +# Basic case: u has global DelegateRights through g1 and ShowConfigTab +# through g2; then DelegateRights revoked from g1. + +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg1: $msg" ); +( $ret, $msg ) = $g1->PrincipalObj->RevokeRight( Right => 'DelegateRights' ); +ok( $ret, "Revoke DelegateRights from g1: $msg" ); +ok( + not( + $pg1->PrincipalObj->HasRight( + Right => 'ShowConfigTab', + Object => $RT::System + ) + ), + "Test personal group 1 lacks ShowConfigTab right after DelegateRights revoked from g1" +); + + + +# Corner case - restricted delegation: u has DelegateRights on pg1 +# through g1 and AdminGroup on pg1 through g2; then DelegateRights +# revoked from g1. + +clear_acls_and_groups(); + +( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights', + Object => $pg1); +ok( $ret, "Grant DelegateRights on pg1 to g1: $msg" ); +( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'AdminGroup', + Object => $pg1); +ok( $ret, "Grant AdminGroup on pg1 to g2: $msg" ); +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); +( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g2: $msg" ); +ok( $u1->PrincipalObj->HasRight( + Right => 'DelegateRights', + Object => $pg1 ), + "test user 1 has DelegateRights on pg1 after joining g1" ); +ok( not( $u1->PrincipalObj->HasRight( + Right => 'DelegateRights', + Object => $RT::System )), + "Test personal group 1 lacks global DelegateRights after joining g1" ); +$ace = RT::ACE->new($u1); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'AdminGroup', + Object => $pg1, + PrincipalType => 'Group', + PrincipalId => $g2->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" ); +ok( $pg1->PrincipalObj->HasRight( + Right => 'AdminGroup', + Object => $pg1 ), + "Test personal group 1 has AdminGroup right on pg1 after delegation" ); +( $ret, $msg ) = $g1->PrincipalObj->RevokeRight ( Right => 'DelegateRights', + Object => $pg1 ); +ok( $ret, "Revoke DelegateRights on pg1 from g1: $msg" ); +ok( not( $pg1->PrincipalObj->HasRight( + Right => 'AdminGroup', + Object => $pg1 )), + "Test personal group 1 lacks AdminGroup right on pg1 after DelegateRights revoked from g1" ); +( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights', + Object => $pg1); + +# Corner case - restricted delegation: u has DelegateRights on pg1 +# through g1 and AdminGroup on pg1 through g2; then u removed from g1. + +ok( $ret, "Grant DelegateRights on pg1 to g1: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" ); +ok( $pg1->PrincipalObj->HasRight( + Right => 'AdminGroup', + Object => $pg1 ), + "Test personal group 1 has AdminGroup right on pg1 after delegation" ); +( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId ); +ok( $ret, "Delete test user 1 from g1: $msg" ); +ok( not( $pg1->PrincipalObj->HasRight( + Right => 'AdminGroup', + Object => $pg1 )), + "Test personal group 1 lacks AdminGroup right on pg1 after user removed from g1" ); + +clear_acls_and_groups(); + + + +# Corner case - multiple delegation rights: u has global +# DelegateRights directly and DelegateRights on pg1 through g1, and +# AdminGroup on pg1 through g2; then u removed from g1 (delegation +# should remain); then DelegateRights revoked from u (delegation +# should not remain). + +( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights', + Object => $pg1); +ok( $ret, "Grant DelegateRights on pg1 to g1: $msg" ); +( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'AdminGroup', + Object => $pg1); +ok( $ret, "Grant AdminGroup on pg1 to g2: $msg" ); +( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights', + Object => $RT::System); +ok( $ret, "Grant DelegateRights to user: $msg" ); +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); +( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g2: $msg" ); +$ace = RT::ACE->new($u1); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'AdminGroup', + Object => $pg1, + PrincipalType => 'Group', + PrincipalId => $g2->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" ); +( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId ); +ok( $ret, "Delete test user 1 from g1: $msg" ); +ok( $pg1->PrincipalObj->HasRight(Right => 'AdminGroup', + Object => $pg1), + "Test personal group 1 retains AdminGroup right on pg1 after user removed from g1" ); +( $ret, $msg ) = $u1->PrincipalObj->RevokeRight( Right => 'DelegateRights', + Object => $RT::System ); +ok( not ($pg1->PrincipalObj->HasRight(Right => 'AdminGroup', + Object => $pg1)), + "Test personal group 1 lacks AdminGroup right on pg1 after DelegateRights revoked"); + +# Corner case - multiple delegation rights and selectivity: u has +# DelegateRights globally and on g2 directly and DelegateRights on pg1 +# through g1, and AdminGroup on pg1 through g2; then global +# DelegateRights revoked from u (delegation should remain), +# DelegateRights on g2 revoked from u (delegation should remain), and +# u removed from g1 (delegation should not remain). + +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); +( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights', + Object => $RT::System); +ok( $ret, "Grant DelegateRights to user: $msg" ); +( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights', + Object => $g2); +ok( $ret, "Grant DelegateRights on g2 to user: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" ); +( $ret, $msg ) = $u1->PrincipalObj->RevokeRight( Right => 'DelegateRights', + Object => $RT::System ); +ok( $pg1->PrincipalObj->HasRight(Right => 'AdminGroup', + Object => $pg1), + "Test personal group 1 retains AdminGroup right on pg1 after global DelegateRights revoked" ); +( $ret, $msg ) = $u1->PrincipalObj->RevokeRight( Right => 'DelegateRights', + Object => $g2 ); +ok( $pg1->PrincipalObj->HasRight(Right => 'AdminGroup', + Object => $pg1), + "Test personal group 1 retains AdminGroup right on pg1 after DelegateRights on g2 revoked" ); +( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId ); +ok( $ret, "Delete test user 1 from g1: $msg" ); +ok( not ($pg1->PrincipalObj->HasRight(Right => 'AdminGroup', + Object => $pg1)), + "Test personal group 1 lacks AdminGroup right on pg1 after user removed from g1"); + + + +# Corner case - indirect delegation rights: u has DelegateRights +# through g1 via g3, and ShowConfigTab via g2; then g3 removed from +# g1. + +clear_acls_and_groups(); + +( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights' ); +ok( $ret, "Grant DelegateRights to g1: $msg" ); +( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'ShowConfigTab' ); +ok( $ret, "Grant ShowConfigTab to g2: $msg" ); +( $ret, $msg ) = $g1->AddMember( $g3->PrincipalId ); +ok( $ret, "Add g3 to g1: $msg" ); +( $ret, $msg ) = $g3->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g3: $msg" ); +( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g2: $msg" ); + +$ace = RT::ACE->new($u1); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'ShowConfigTab', + Object => $RT::System, + PrincipalType => 'Group', + PrincipalId => $g2->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg1: $msg" ); + +( $ret, $msg ) = $g1->DeleteMember( $g3->PrincipalId ); +ok( $ret, "Delete g3 from g1: $msg" ); +ok( not ($pg1->PrincipalObj->HasRight(Right => 'ShowConfigTab', + Object => $RT::System)), + "Test personal group 1 lacks ShowConfigTab right after g3 removed from g1"); + +# Corner case - indirect delegation rights: u has DelegateRights +# through g1 via g3, and ShowConfigTab via g2; then DelegateRights +# revoked from g1. + +( $ret, $msg ) = $g1->AddMember( $g3->PrincipalId ); +ok( $ret, "Add g3 to g1: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg1: $msg" ); +( $ret, $msg ) = $g1->PrincipalObj->RevokeRight ( Right => 'DelegateRights' ); +ok( $ret, "Revoke DelegateRights from g1: $msg" ); + +ok( not ($pg1->PrincipalObj->HasRight(Right => 'ShowConfigTab', + Object => $RT::System)), + "Test personal group 1 lacks ShowConfigTab right after DelegateRights revoked from g1"); + + + +# Corner case - delegation of DelegateRights: u1 has DelegateRights +# via g1 and delegates DelegateRights to pg1; u2 has DelegateRights +# via pg1 and ShowConfigTab via g2; then u1 removed from g1. + +clear_acls_and_groups(); + +( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights' ); +ok( $ret, "Grant DelegateRights to g1: $msg" ); +( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'ShowConfigTab' ); +ok( $ret, "Grant ShowConfigTab to g2: $msg" ); +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); +$ace = RT::ACE->new($u1); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'DelegateRights', + Object => $RT::System, + PrincipalType => 'Group', + PrincipalId => $g1->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate DelegateRights to pg1: $msg" ); + +( $ret, $msg ) = $pg1->AddMember( $u2->PrincipalId ); +ok( $ret, "Add test user 2 to pg1: $msg" ); +( $ret, $msg ) = $g2->AddMember( $u2->PrincipalId ); +ok( $ret, "Add test user 2 to g2: $msg" ); +$ace = RT::ACE->new($u2); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'ShowConfigTab', + Object => $RT::System, + PrincipalType => 'Group', + PrincipalId => $g2->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg2: $msg" ); + +ok( $pg2->PrincipalObj->HasRight(Right => 'ShowConfigTab', + Object => $RT::System), + "Test personal group 2 has ShowConfigTab right after delegation"); +( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId ); +ok( $ret, "Delete u1 from g1: $msg" ); +ok( not ($pg2->PrincipalObj->HasRight(Right => 'ShowConfigTab', + Object => $RT::System)), + "Test personal group 2 lacks ShowConfigTab right after u1 removed from g1"); + +# Corner case - delegation of DelegateRights: u1 has DelegateRights +# via g1 and delegates DelegateRights to pg1; u2 has DelegateRights +# via pg1 and ShowConfigTab via g2; then DelegateRights revoked from +# g1. + +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add u1 to g1: $msg" ); +$ace = RT::ACE->new($u1); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'DelegateRights', + Object => $RT::System, + PrincipalType => 'Group', + PrincipalId => $g1->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate DelegateRights to pg1: $msg" ); +$ace = RT::ACE->new($u2); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'ShowConfigTab', + Object => $RT::System, + PrincipalType => 'Group', + PrincipalId => $g2->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg2: $msg" ); + +( $ret, $msg ) = $g1->PrincipalObj->RevokeRight ( Right => 'DelegateRights' ); +ok( $ret, "Revoke DelegateRights from g1: $msg" ); +ok( not ($pg2->PrincipalObj->HasRight(Right => 'ShowConfigTab', + Object => $RT::System)), + "Test personal group 2 lacks ShowConfigTab right after DelegateRights revoked from g1"); + + + + +####### + +sub clear_acls_and_groups { + # Revoke all rights granted to our cast + my $acl = RT::ACL->new($RT::SystemUser); + foreach (@principals) { + $acl->LimitToPrincipal(Type => $$_->PrincipalObj->PrincipalType, + Id => $$_->PrincipalObj->Id); + } + while (my $ace = $acl->Next()) { + $ace->Delete(); + } + + # Remove all group memberships + my $members = RT::GroupMembers->new($RT::SystemUser); + foreach (@groups) { + $members->LimitToMembersOfGroup( $$_->PrincipalId ); + } + while (my $member = $members->Next()) { + $member->Delete(); + } + + $acl->RedoSearch(); + is( $acl->Count() , 0, + "All principals have no rights after clearing ACLs" ); + $members->RedoSearch(); + is( $members->Count() , 0, + "All groups have no members after clearing groups" ); +} diff --git a/rt/t/delegation/revocation.t b/rt/t/delegation/revocation.t new file mode 100644 index 000000000..151525edf --- /dev/null +++ b/rt/t/delegation/revocation.t @@ -0,0 +1,135 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT; + +use RT::Test tests => 22; + +my ($u1, $g1, $pg1, $pg2, $ace, @groups, @users, @principals); +@groups = (\$g1, \$pg1, \$pg2); +@users = (\$u1); +@principals = (@groups, @users); + +my($ret, $msg); + +$u1 = RT::User->new($RT::SystemUser); +( $ret, $msg ) = $u1->LoadOrCreateByEmail('delegtest1@example.com'); +ok( $ret, "Load / Create test user 1: $msg" ); +$u1->SetPrivileged(1); + +$g1 = RT::Group->new($RT::SystemUser); +( $ret, $msg) = $g1->LoadUserDefinedGroup('dg1'); +unless ($ret) { + ( $ret, $msg ) = $g1->CreateUserDefinedGroup( Name => 'dg1' ); +} +$pg1 = RT::Group->new($RT::SystemUser); +( $ret, $msg ) = $pg1->LoadPersonalGroup( Name => 'dpg1', + User => $u1->PrincipalId ); +unless ($ret) { + ( $ret, $msg ) = $pg1->CreatePersonalGroup( Name => 'dpg1', + PrincipalId => $u1->PrincipalId ); +} +ok( $ret, "Load / Create test personal group 1: $msg" ); +$pg2 = RT::Group->new($RT::SystemUser); +( $ret, $msg ) = $pg2->LoadPersonalGroup( Name => 'dpg2', + User => $u1->PrincipalId ); +unless ($ret) { + ( $ret, $msg ) = $pg2->CreatePersonalGroup( Name => 'dpg2', + PrincipalId => $u1->PrincipalId ); +} +ok( $ret, "Load / Create test personal group 2: $msg" ); + +clear_acls_and_groups(); + +( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights' ); +ok( $ret, "Grant DelegateRights to u1: $msg" ); +( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'ShowConfigTab' ); +ok( $ret, "Grant ShowConfigTab to g1: $msg" ); +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); + +$ace = RT::ACE->new($u1); +( $ret, $msg ) = $ace->LoadByValues( + RightName => 'ShowConfigTab', + Object => $RT::System, + PrincipalType => 'Group', + PrincipalId => $g1->PrincipalId +); +ok( $ret, "Look up ACE to be delegated: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg1: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg2: $msg" ); + +ok(( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System ) and + $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System )), + "Test personal groups have ShowConfigTab right after delegation" ); + +( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId ); +ok( $ret, "Delete test user 1 from g1: $msg" ); + +ok( not( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System )), + "Test personal group 1 lacks ShowConfigTab after user removed from g1" ); +ok( not( $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System )), + "Test personal group 2 lacks ShowConfigTab after user removed from g1" ); + +( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId ); +ok( $ret, "Add test user 1 to g1: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg1: $msg" ); +( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId ); +ok( $ret, "Delegate ShowConfigTab to pg2: $msg" ); + +ok(( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System ) and + $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System )), + "Test personal groups have ShowConfigTab right after delegation" ); + +( $ret, $msg ) = $g1->PrincipalObj->RevokeRight( Right => 'ShowConfigTab' ); +ok( $ret, "Revoke ShowConfigTab from g1: $msg" ); + +ok( not( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System )), + "Test personal group 1 lacks ShowConfigTab after user removed from g1" ); +ok( not( $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab', + Object => $RT::System )), + "Test personal group 2 lacks ShowConfigTab after user removed from g1" ); + + + +####### + +sub clear_acls_and_groups { + # Revoke all rights granted to our cast + my $acl = RT::ACL->new($RT::SystemUser); + foreach (@principals) { + $acl->LimitToPrincipal(Type => $$_->PrincipalObj->PrincipalType, + Id => $$_->PrincipalObj->Id); + } + while (my $ace = $acl->Next()) { + $ace->Delete(); + } + + # Remove all group memberships + my $members = RT::GroupMembers->new($RT::SystemUser); + foreach (@groups) { + $members->LimitToMembersOfGroup( $$_->PrincipalId ); + } + while (my $member = $members->Next()) { + $member->Delete(); + } + + $acl->RedoSearch(); + is( $acl->Count() , 0, + "All principals have no rights after clearing ACLs" ); + $members->RedoSearch(); + is( $members->Count() , 0, + "All groups have no members after clearing groups" ); +} diff --git a/rt/t/i18n/default.t b/rt/t/i18n/default.t new file mode 100644 index 000000000..6c9842a4c --- /dev/null +++ b/rt/t/i18n/default.t @@ -0,0 +1,19 @@ +#!/usr/bin/perl -w +use strict; +use warnings; + +use RT::Test tests => 8; + +my ($baseurl, $m) = RT::Test->started_ok; +$m->get_ok('/'); +$m->title_is('Login'); + +$m->get_ok('/', { 'Accept-Language' => 'x-klingon' }); +$m->title_is('Login', 'unavailable language fallback to en'); + +$m->add_header('Accept-Language' => 'zh-tw,zh;q=0.8,en-gb;q=0.5,en;q=0.3'); +$m->get_ok('/'); +use utf8; +Encode::_utf8_on($m->{content}); +$m->title_is('登入', 'Page title properly translated to chinese'); +$m->content_contains('密碼','Password properly translated'); diff --git a/rt/t/mail/charsets-outgoing.t b/rt/t/mail/charsets-outgoing.t new file mode 100644 index 000000000..ca44bbd27 --- /dev/null +++ b/rt/t/mail/charsets-outgoing.t @@ -0,0 +1,306 @@ +#!/usr/bin/perl +use strict; +use warnings; +use utf8; + +use RT::Test tests => 30; + + +RT::Test->set_mail_catcher; + +my $queue = RT::Test->load_or_create_queue( + Name => 'Regression', + CorrespondAddress => 'rt-recipient@example.com', + CommentAddress => 'rt-recipient@example.com', +); +ok $queue && $queue->id, 'loaded or created queue'; + +diag "make sure queue has no subject tag" if $ENV{'TEST_VERBOSE'}; +{ + my ($status, $msg) = $queue->SetSubjectTag( undef ); + ok $status, "set subject tag for the queue" or diag "error: $msg"; +} + +diag "set intial simple autoreply template" if $ENV{'TEST_VERBOSE'}; +{ + my $template = RT::Template->new( $RT::SystemUser ); + $template->Load('Autoreply'); + ok $template->id, "loaded autoreply tempalte"; + + my ($status, $msg) = $template->SetContent( + "Subject: Autreply { \$Ticket->Subject }\n" + ."\n" + ."hi there it's an autoreply.\n" + ."\n" + ); + ok $status, "changed content of the template" + or diag "error: $msg"; +} + +diag "basic test of autoreply" if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => 'test', + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; +} + +my $str_ru_test = "\x{442}\x{435}\x{441}\x{442}"; +my $str_ru_autoreply = "\x{410}\x{432}\x{442}\x{43e}\x{43e}\x{442}\x{432}\x{435}\x{442}"; +my $str_ru_support = "\x{43f}\x{43e}\x{434}\x{434}\x{435}\x{440}\x{436}\x{43a}\x{430}"; + +diag "non-ascii Subject with ascii prefix set in the template" + if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => $str_ru_test, + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_test/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +diag "set non-ascii subject tag for the queue" if $ENV{'TEST_VERBOSE'}; +{ + my ($status, $msg) = $queue->SetSubjectTag( $str_ru_support ); + ok $status, "set subject tag for the queue" or diag "error: $msg"; +} + +diag "ascii subject with non-ascii subject tag" if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => 'test', + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_support/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +diag "non-ascii subject with non-ascii subject tag" if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => $str_ru_test, + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_support/ + or do { $status = 0; diag "wrong subject: $subject" }; + $subject =~ /$str_ru_test/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +diag "return back the empty subject tag" if $ENV{'TEST_VERBOSE'}; +{ + my ($status, $msg) = $queue->SetSubjectTag( undef ); + ok $status, "set subject tag for the queue" or diag "error: $msg"; +} + +diag "add non-ascii subject prefix in the autoreply template" if $ENV{'TEST_VERBOSE'}; +{ + my $template = RT::Template->new( $RT::SystemUser ); + $template->Load('Autoreply'); + ok $template->id, "loaded autoreply tempalte"; + + my ($status, $msg) = $template->SetContent( + "Subject: $str_ru_autoreply { \$Ticket->Subject }\n" + ."\n" + ."hi there it's an autoreply.\n" + ."\n" + ); + ok $status, "changed content of the template" or diag "error: $msg"; +} + +diag "ascii subject with non-ascii subject prefix in template" if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => 'test', + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_autoreply/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +diag "non-ascii subject with non-ascii subject prefix in template" + if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => $str_ru_test, + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_autoreply/ + or do { $status = 0; diag "wrong subject: $subject" }; + $subject =~ /$str_ru_autoreply/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +diag "set non-ascii subject tag for the queue" if $ENV{'TEST_VERBOSE'}; +{ + my ($status, $msg) = $queue->SetSubjectTag( $str_ru_support ); + ok $status, "set subject tag for the queue" or diag "error: $msg"; +} + +diag "non-ascii subject, non-ascii prefix in template and non-ascii tag" + if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => $str_ru_test, + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_autoreply/ + or do { $status = 0; diag "wrong subject: $subject" }; + $subject =~ /$str_ru_autoreply/ + or do { $status = 0; diag "wrong subject: $subject" }; + $subject =~ /$str_ru_autoreply/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +diag "flush subject tag of the queue" if $ENV{'TEST_VERBOSE'}; +{ + my ($status, $msg) = $queue->SetSubjectTag( undef ); + ok $status, "set subject tag for the queue" or diag "error: $msg"; +} + + +diag "don't change subject via template" if $ENV{'TEST_VERBOSE'}; +{ + my $template = RT::Template->new( $RT::SystemUser ); + $template->Load('Autoreply'); + ok $template->id, "loaded autoreply tempalte"; + + my ($status, $msg) = $template->SetContent( + "\n" + ."\n" + ."hi there it's an autoreply.\n" + ."\n" + ); + ok $status, "changed content of the template" or diag "error: $msg"; +} + +diag "non-ascii Subject without changes in template" if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => $str_ru_test, + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_test/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +diag "set non-ascii subject tag for the queue" if $ENV{'TEST_VERBOSE'}; +{ + my ($status, $msg) = $queue->SetSubjectTag( $str_ru_support ); + ok $status, "set subject tag for the queue" or diag "error: $msg"; +} + +diag "non-ascii Subject without changes in template and with non-ascii subject tag" + if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Create( + Queue => $queue->id, + Subject => $str_ru_test, + Requestor => 'root@localhost', + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = Encode::decode_utf8( $entity->head->get('Subject') ); + $subject =~ /$str_ru_test/ + or do { $status = 0; diag "wrong subject: $subject" }; + $subject =~ /$str_ru_support/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "all mails have correct data"; +} + +sub parse_mail { + my $mail = shift; + require RT::EmailParser; + my $parser = new RT::EmailParser; + $parser->ParseMIMEEntityFromScalar( $mail ); + return $parser->Entity; +} + diff --git a/rt/t/mail/crypt-gnupg.t b/rt/t/mail/crypt-gnupg.t new file mode 100644 index 000000000..f33fbab1c --- /dev/null +++ b/rt/t/mail/crypt-gnupg.t @@ -0,0 +1,312 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use RT::Test nodata => 1, tests => 92; +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use File::Spec (); +use Cwd; + +my $homedir = RT::Test::get_abs_relocatable_dir(File::Spec->updir(), + qw(data gnupg keyrings) ); + +mkdir $homedir; + +use_ok('RT::Crypt::GnuPG'); +use_ok('MIME::Entity'); + +RT->Config->Set( 'GnuPG', + Enable => 1, + OutgoingMessagesFormat => 'RFC' ); + +RT->Config->Set( 'GnuPGOptions', + homedir => $homedir, + 'no-permission-warning' => undef, +); + + +diag 'only signing. correct passphrase' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' ); + ok( $entity, 'signed entity'); + ok( !$res{'logger'}, "log is here as well" ) or diag $res{'logger'}; + my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} ); + is( scalar @status, 2, 'two records: passphrase, signing'); + is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct'); + is( $status[0]->{'Status'}, 'DONE', 'good passphrase'); + is( $status[1]->{'Operation'}, 'Sign', 'operation is correct'); + is( $status[1]->{'Status'}, 'DONE', 'done'); + is( $status[1]->{'User'}->{'EmailAddress'}, 'rt@example.com', 'correct email'); + + ok( $entity->is_multipart, 'signed message is multipart' ); + is( $entity->parts, 2, 'two parts' ); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 1, 'one protected part' ); + is( $parts[0]->{'Type'}, 'signed', "have signed part" ); + is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" ); + is( $parts[0]->{'Top'}, $entity, "it's the same entity" ); + + my @res = RT::Crypt::GnuPG::VerifyDecrypt( Entity => $entity ); + is scalar @res, 1, 'one operation'; + @status = RT::Crypt::GnuPG::ParseStatus( $res[0]{'status'} ); + is( scalar @status, 1, 'one record'); + is( $status[0]->{'Operation'}, 'Verify', 'operation is correct'); + is( $status[0]->{'Status'}, 'DONE', 'good passphrase'); + is( $status[0]->{'Trust'}, 'ULTIMATE', 'have trust value'); +} + +diag 'only signing. missing passphrase' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => '' ); + ok( $res{'exit_code'}, "couldn't sign without passphrase"); + ok( $res{'error'} || $res{'logger'}, "error is here" ); + + my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} ); + is( scalar @status, 1, 'one record'); + is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct'); + is( $status[0]->{'Status'}, 'MISSING', 'missing passphrase'); +} + +diag 'only signing. wrong passphrase' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'wrong' ); + ok( $res{'exit_code'}, "couldn't sign with bad passphrase"); + ok( $res{'error'} || $res{'logger'}, "error is here" ); + + my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} ); + is( scalar @status, 1, 'one record'); + is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct'); + is( $status[0]->{'Status'}, 'BAD', 'wrong passphrase'); +} + +diag 'encryption only' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 ); + ok( !$res{'exit_code'}, "successful encryption" ); + ok( !$res{'logger'}, "no records in logger" ); + + my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} ); + is( scalar @status, 1, 'one record'); + is( $status[0]->{'Operation'}, 'Encrypt', 'operation is correct'); + is( $status[0]->{'Status'}, 'DONE', 'done'); + + ok($entity, 'get an encrypted part'); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 1, 'one protected part' ); + is( $parts[0]->{'Type'}, 'encrypted', "have encrypted part" ); + is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" ); + is( $parts[0]->{'Top'}, $entity, "it's the same entity" ); +} + +diag 'encryption only, bad recipient' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'keyless@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 ); + ok( $res{'exit_code'}, 'no way to encrypt without keys of recipients'); + ok( $res{'logger'}, "errors are in logger" ); + + my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} ); + is( scalar @status, 1, 'one record'); + is( $status[0]->{'Keyword'}, 'INV_RECP', 'invalid recipient'); +} + +diag 'encryption and signing with combined method' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Passphrase => 'test' ); + ok( !$res{'exit_code'}, "successful encryption with signing" ); + ok( !$res{'logger'}, "no records in logger" ); + + my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} ); + is( scalar @status, 3, 'three records: passphrase, sign and encrypt'); + is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct'); + is( $status[0]->{'Status'}, 'DONE', 'done'); + is( $status[1]->{'Operation'}, 'Sign', 'operation is correct'); + is( $status[1]->{'Status'}, 'DONE', 'done'); + is( $status[2]->{'Operation'}, 'Encrypt', 'operation is correct'); + is( $status[2]->{'Status'}, 'DONE', 'done'); + + ok($entity, 'get an encrypted and signed part'); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 1, 'one protected part' ); + is( $parts[0]->{'Type'}, 'encrypted', "have encrypted part" ); + is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" ); + is( $parts[0]->{'Top'}, $entity, "it's the same entity" ); +} + +diag 'encryption and signing with cascading, sign on encrypted' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 ); + ok( !$res{'exit_code'}, 'successful encryption' ); + ok( !$res{'logger'}, "no records in logger" ); + %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' ); + ok( !$res{'exit_code'}, 'successful signing' ); + ok( !$res{'logger'}, "no records in logger" ); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 1, 'one protected part, top most' ); + is( $parts[0]->{'Type'}, 'signed', "have signed part" ); + is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" ); + is( $parts[0]->{'Top'}, $entity, "it's the same entity" ); +} + +diag 'find signed/encrypted part deep inside' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 ); + ok( !$res{'exit_code'}, "success" ); + $entity->make_multipart( 'mixed', Force => 1 ); + $entity->attach( + Type => 'text/plain', + Data => ['-'x76, 'this is mailing list'], + ); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 1, 'one protected part' ); + is( $parts[0]->{'Type'}, 'encrypted', "have encrypted part" ); + is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" ); + is( $parts[0]->{'Top'}, $entity->parts(0), "it's the same entity" ); +} + +diag 'wrong signed/encrypted parts: no protocol' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 ); + ok( !$res{'exit_code'}, 'success' ); + $entity->head->mime_attr( 'Content-Type.protocol' => undef ); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 0, 'no protected parts' ); +} + +diag 'wrong signed/encrypted parts: not enought parts' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 ); + ok( !$res{'exit_code'}, 'success' ); + $entity->parts([ $entity->parts(0) ]); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 0, 'no protected parts' ); +} + +diag 'wrong signed/encrypted parts: wrong proto' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 ); + ok( !$res{'exit_code'}, 'success' ); + $entity->head->mime_attr( 'Content-Type.protocol' => 'application/bad-proto' ); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 0, 'no protected parts' ); +} + +diag 'wrong signed/encrypted parts: wrong proto' if $ENV{'TEST_VERBOSE'}; +{ + my $entity = MIME::Entity->build( + From => 'rt@example.com', + To => 'rt@example.com', + Subject => 'test', + Data => ['test'], + ); + my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' ); + ok( !$res{'exit_code'}, 'success' ); + $entity->head->mime_attr( 'Content-Type.protocol' => 'application/bad-proto' ); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 0, 'no protected parts' ); +} + +diag 'verify inline and in attachment signatures' if $ENV{'TEST_VERBOSE'}; +{ + open my $fh, "$homedir/signed_old_style_with_attachment.eml"; + my $parser = new MIME::Parser; + my $entity = $parser->parse( $fh ); + + my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity ); + is( scalar @parts, 2, 'two protected parts' ); + is( $parts[1]->{'Type'}, 'signed', "have signed part" ); + is( $parts[1]->{'Format'}, 'Inline', "inline format" ); + is( $parts[1]->{'Data'}, $entity->parts(0), "it's first part" ); + + is( $parts[0]->{'Type'}, 'signed', "have signed part" ); + is( $parts[0]->{'Format'}, 'Attachment', "attachment format" ); + is( $parts[0]->{'Data'}, $entity->parts(1), "data in second part" ); + is( $parts[0]->{'Signature'}, $entity->parts(2), "file's signature in third part" ); + + my @res = RT::Crypt::GnuPG::VerifyDecrypt( Entity => $entity ); + my @status = RT::Crypt::GnuPG::ParseStatus( $res[0]->{'status'} ); + is( scalar @status, 1, 'one record'); + is( $status[0]->{'Operation'}, 'Verify', 'operation is correct'); + is( $status[0]->{'Status'}, 'DONE', 'good passphrase'); + is( $status[0]->{'Trust'}, 'ULTIMATE', 'have trust value'); + + $parser->filer->purge(); +} + diff --git a/rt/t/mail/extractsubjecttag.t b/rt/t/mail/extractsubjecttag.t new file mode 100644 index 000000000..5a2548883 --- /dev/null +++ b/rt/t/mail/extractsubjecttag.t @@ -0,0 +1,98 @@ +#!/usr/bin/perl +use strict; +use warnings; +use utf8; + +use RT::Test tests => 14; + + +my ($baseurl, $m) = RT::Test->started_ok; +RT::Test->set_mail_catcher; + +my $queue = RT::Test->load_or_create_queue( + Name => 'Regression', + CorrespondAddress => 'rt-recipient@example.com', + CommentAddress => 'rt-recipient@example.com', +); +my $subject_tag = 'Windows/Servers-Desktops'; +ok $queue && $queue->id, 'loaded or created queue'; + +diag "Set Subject Tag" if $ENV{'TEST_VERBOSE'}; +{ + is(RT->System->SubjectTag($queue), undef, 'No Subject Tag yet'); + my ($status, $msg) = $queue->SetSubjectTag( $subject_tag ); + ok $status, "set subject tag for the queue" or diag "error: $msg"; + is(RT->System->SubjectTag($queue), $subject_tag, "Set Subject Tag to $subject_tag"); +} + +my $original_ticket = RT::Ticket->new( $RT::SystemUser ); +diag "Create a ticket and make sure it has the subject tag" if $ENV{'TEST_VERBOSE'}; +{ + $original_ticket->Create( + Queue => $queue->id, + Subject => 'test', + Requestor => 'root@localhost' + ); + my @mails = RT::Test->fetch_caught_mails; + ok @mails, "got some outgoing emails"; + + my $status = 1; + foreach my $mail ( @mails ) { + my $entity = parse_mail( $mail ); + my $subject = $entity->head->get('Subject'); + $subject =~ /\[\Q$subject_tag\E #\d+\]/ + or do { $status = 0; diag "wrong subject: $subject" }; + } + ok $status, "Correctly added subject tag to ticket"; +} + + +diag "Test that a reply with a Subject Tag doesn't change the subject" if $ENV{'TEST_VERBOSE'}; +{ + my $ticketid = $original_ticket->Id; + my $text = <<EOF; +From: root\@localhost +To: general\@$RT::rtname +Subject: [$subject_tag #$ticketid] test + +reply with subject tag +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text, queue => $queue->Name); + is ($status >> 8, 0, "The mail gateway exited normally"); + is ($id, $ticketid, "Replied to ticket $id correctly"); + + my $freshticket = RT::Ticket->new( $RT::SystemUser ); + $freshticket->LoadById($id); + is($original_ticket->Subject,$freshticket->Subject,'Stripped Queue Subject Tag correctly'); + +} + +diag "Test that a reply with another RT's subject tag changes the subject" if $ENV{'TEST_VERBOSE'}; +{ + my $ticketid = $original_ticket->Id; + my $text = <<EOF; +From: root\@localhost +To: general\@$RT::rtname +Subject: [$subject_tag #$ticketid] [remote-rt-system #79] test + +reply with subject tag and remote rt subject tag +EOF + diag($text); + my ($status, $id) = RT::Test->send_via_mailgate($text, queue => $queue->Name); + is ($status >> 8, 0, "The mail gateway exited normally"); + is ($id, $ticketid, "Replied to ticket $id correctly"); + + my $freshticket = RT::Ticket->new( $RT::SystemUser ); + $freshticket->LoadById($id); + like($freshticket->Subject,qr/\[remote-rt-system #79\]/,"Kept remote rt's subject tag"); + unlike($freshticket->Subject,qr/\[\Q$subject_tag\E #$ticketid\]/,'Stripped Queue Subject Tag correctly'); + +} + +sub parse_mail { + my $mail = shift; + require RT::EmailParser; + my $parser = new RT::EmailParser; + $parser->ParseMIMEEntityFromScalar( $mail ); + return $parser->Entity; +} diff --git a/rt/t/mail/gateway.t b/rt/t/mail/gateway.t new file mode 100644 index 000000000..00de1ec7f --- /dev/null +++ b/rt/t/mail/gateway.t @@ -0,0 +1,802 @@ +#!/usr/bin/perl -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC +# <jesse.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/copyleft/gpl.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + +rt-mailgate - Mail interface to RT3. + +=cut + +use strict; +use warnings; + + +use RT::Test config => 'Set( $UnsafeEmailCommands, 1);', tests => 159; +my ($baseurl, $m) = RT::Test->started_ok; + +use RT::Tickets; + +use MIME::Entity; +use Digest::MD5 qw(md5_base64); +use LWP::UserAgent; + +# TODO: --extension queue + +my $url = $m->rt_base_url; + +diag "Make sure that when we call the mailgate without URL, it fails" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of new ticket creation + +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text, url => undef); + is ($status >> 8, 1, "The mail gateway exited with a failure"); + ok (!$id, "No ticket id") or diag "by mistake ticket #$id"; +} + +diag "Make sure that when we call the mailgate with wrong URL, it tempfails" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of new ticket creation + +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text, url => 'http://this.test.for.non-connection.is.expected.to.generate.an.error'); + is ($status >> 8, 75, "The mail gateway exited with a failure"); + ok (!$id, "No ticket id"); +} + +my $everyone_group; +diag "revoke rights tests depend on" if $ENV{'TEST_VERBOSE'}; +{ + $everyone_group = RT::Group->new( $RT::SystemUser ); + $everyone_group->LoadSystemInternalGroup( 'Everyone' ); + ok ($everyone_group->Id, "Found group 'everyone'"); + + foreach( qw(CreateTicket ReplyToTicket CommentOnTicket) ) { + $everyone_group->PrincipalObj->RevokeRight(Right => $_); + } +} + +diag "Test new ticket creation by root who is privileged and superuser" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of new ticket creation + +Blah! +Foob! +EOF + + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "Created ticket"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + is ($tick->Id, $id, "correct ticket id"); + is ($tick->Subject , 'This is a test of new ticket creation', "Created the ticket"); +} + +diag "Test the 'X-RT-Mail-Extension' field in the header of a ticket" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of the X-RT-Mail-Extension field +Blah! +Foob! +EOF + local $ENV{'EXTENSION'} = "bad value with\nnewlines\n"; + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "Created ticket #$id"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + is ($tick->Id, $id, "correct ticket id"); + is ($tick->Subject, 'This is a test of the X-RT-Mail-Extension field', "Created the ticket"); + + my $transactions = $tick->Transactions; + $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' }); + $transactions->Limit( FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord'); + my $txn = $transactions->First; + isa_ok ($txn, 'RT::Transaction'); + is ($txn->Type, 'Create', "correct type"); + + my $attachment = $txn->Attachments->First; + isa_ok ($attachment, 'RT::Attachment'); + # XXX: We eat all newlines in header, that's not what RFC's suggesting + is ( + $attachment->GetHeader('X-RT-Mail-Extension'), + "bad value with newlines", + 'header is in place, without trailing newline char' + ); +} + +diag "Make sure that not standard --extension is passed" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of new ticket creation + +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'some-extension-arg' ); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "Created ticket #$id"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + is ($tick->Id, $id, "correct ticket id"); + + my $transactions = $tick->Transactions; + $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' }); + $transactions->Limit( FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord'); + my $txn = $transactions->First; + isa_ok ($txn, 'RT::Transaction'); + is ($txn->Type, 'Create', "correct type"); + + my $attachment = $txn->Attachments->First; + isa_ok ($attachment, 'RT::Attachment'); + is ( + $attachment->GetHeader('X-RT-Mail-Extension'), + 'some-extension-arg', + 'header is in place' + ); +} + +diag "Test new ticket creation without --action argument" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rt\@$RT::rtname +Subject: using mailgate without --action arg + +Blah! +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'some-extension-arg' ); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "Created ticket #$id"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + is ($tick->Id, $id, "correct ticket id"); + is ($tick->Subject, 'using mailgate without --action arg', "using mailgate without --action arg"); +} + +diag "This is a test of new ticket creation as an unknown user" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of new ticket creation as an unknown user + +Blah! +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok (!$id, "no ticket created"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ".$tick->Id); + isnt ($tick->Subject , 'This is a test of new ticket creation as an unknown user', "failed to create the new ticket from an unprivileged account"); + + my $u = RT::User->new($RT::SystemUser); + $u->Load("doesnotexist\@@{[RT->Config->Get('rtname')]}"); + ok( !$u->Id, "user does not exist and was not created by failed ticket submission"); +} + +diag "grant everybody with CreateTicket right" if $ENV{'TEST_VERBOSE'}; +{ + ok( RT::Test->set_rights( + { Principal => $everyone_group->PrincipalObj, + Right => [qw(CreateTicket)], + }, + ), "Granted everybody the right to create tickets"); +} + +my $ticket_id; +diag "now everybody can create tickets. can a random unkown user create tickets?" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of new ticket creation as an unknown user + +Blah! +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "ticket created"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ".$tick->Id); + is ($tick->Id, $id, "correct ticket id"); + is ($tick->Subject , 'This is a test of new ticket creation as an unknown user', "failed to create the new ticket from an unprivileged account"); + + my $u = RT::User->new( $RT::SystemUser ); + $u->Load( "doesnotexist\@@{[RT->Config->Get('rtname')]}" ); + ok ($u->Id, "user does not exist and was created by ticket submission"); + $ticket_id = $id; +} + +diag "can another random reply to a ticket without being granted privs? answer should be no." if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist-2\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a reply as an unknown user + +Blah! (Should not work.) +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok (!$id, "no way to reply to the ticket"); + + my $u = RT::User->new($RT::SystemUser); + $u->Load('doesnotexist-2@'.RT->Config->Get('rtname')); + ok( !$u->Id, " user does not exist and was not created by ticket correspondence submission"); +} + +diag "grant everyone 'ReplyToTicket' right" if $ENV{'TEST_VERBOSE'}; +{ + ok( RT::Test->set_rights( + { Principal => $everyone_group->PrincipalObj, + Right => [qw(CreateTicket ReplyToTicket)], + }, + ), "Granted everybody the right to reply to tickets" ); +} + +diag "can another random reply to a ticket after being granted privs? answer should be yes" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist-2\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a reply as an unknown user + +Blah! +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + is ($id, $ticket_id, "replied to the ticket"); + + my $u = RT::User->new($RT::SystemUser); + $u->Load('doesnotexist-2@'.RT->Config->Get('rtname')); + ok ($u->Id, "user exists and was created by ticket correspondence submission"); +} + +diag "add a reply to the ticket using '--extension ticket' feature" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist-2\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of a reply as an unknown user + +Blah! +Foob! +EOF + local $ENV{'EXTENSION'} = $ticket_id; + my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'ticket'); + is ($status >> 8, 0, "The mail gateway exited normally"); + is ($id, $ticket_id, "replied to the ticket"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ".$tick->Id); + is ($tick->Id, $id, "correct ticket id"); + + my $transactions = $tick->Transactions; + $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' }); + $transactions->Limit( FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord'); + my $txn = $transactions->First; + isa_ok ($txn, 'RT::Transaction'); + is ($txn->Type, 'Correspond', "correct type"); + + my $attachment = $txn->Attachments->First; + isa_ok ($attachment, 'RT::Attachment'); + is ($attachment->GetHeader('X-RT-Mail-Extension'), $id, 'header is in place'); +} + +diag "can another random comment on a ticket without being granted privs? answer should be no" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist-3\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a comment as an unknown user + +Blah! (Should not work.) +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text, action => 'comment'); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok (!$id, "no way to comment on the ticket"); + + my $u = RT::User->new($RT::SystemUser); + $u->Load('doesnotexist-3@'.RT->Config->Get('rtname')); + ok( !$u->Id, " user does not exist and was not created by ticket comment submission"); +} + + +diag "grant everyone 'CommentOnTicket' right" if $ENV{'TEST_VERBOSE'}; +{ + ok( RT::Test->set_rights( + { Principal => $everyone_group->PrincipalObj, + Right => [qw(CreateTicket ReplyToTicket CommentOnTicket)], + }, + ), "Granted everybody the right to comment on tickets"); +} + +diag "can another random reply to a ticket after being granted privs? answer should be yes" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist-3\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a comment as an unknown user + +Blah! +Foob! +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text, action => 'comment'); + is ($status >> 8, 0, "The mail gateway exited normally"); + is ($id, $ticket_id, "replied to the ticket"); + + my $u = RT::User->new($RT::SystemUser); + $u->Load('doesnotexist-3@'.RT->Config->Get('rtname')); + ok ($u->Id, " user exists and was created by ticket comment submission"); +} + +diag "add comment to the ticket using '--extension action' feature" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: doesnotexist-3\@@{[RT->Config->Get('rtname')]} +To: rt\@@{[RT->Config->Get('rtname')]} +Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a comment via '--extension action' + +Blah! +Foob! +EOF + local $ENV{'EXTENSION'} = 'comment'; + my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'action'); + is ($status >> 8, 0, "The mail gateway exited normally"); + is ($id, $ticket_id, "added comment to the ticket"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ".$tick->Id); + is ($tick->Id, $id, "correct ticket id"); + + my $transactions = $tick->Transactions; + $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' }); + $transactions->Limit( + FIELD => 'Type', + OPERATOR => 'NOT ENDSWITH', + VALUE => 'EmailRecord', + ENTRYAGGREGATOR => 'AND', + ); + my $txn = $transactions->First; + isa_ok ($txn, 'RT::Transaction'); + is ($txn->Type, 'Comment', "correct type"); + + my $attachment = $txn->Attachments->First; + isa_ok ($attachment, 'RT::Attachment'); + is ($attachment->GetHeader('X-RT-Mail-Extension'), 'comment', 'header is in place'); +} + +diag "Testing preservation of binary attachments" if $ENV{'TEST_VERBOSE'}; +{ + # Get a binary blob (Best Practical logo) + my $LOGO_FILE = $RT::MasonComponentRoot .'/NoAuth/images/bplogo.gif'; + + # Create a mime entity with an attachment + my $entity = MIME::Entity->build( + From => 'root@localhost', + To => 'rt@localhost', + Subject => 'binary attachment test', + Data => ['This is a test of a binary attachment'], + ); + + $entity->attach( + Path => $LOGO_FILE, + Type => 'image/gif', + Encoding => 'base64', + ); + # Create a ticket with a binary attachment + my ($status, $id) = RT::Test->send_via_mailgate($entity); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "created ticket"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ".$tick->Id); + is ($tick->Id, $id, "correct ticket id"); + is ($tick->Subject , 'binary attachment test', "Created the ticket - ".$tick->Id); + + my $file = `cat $LOGO_FILE`; + ok ($file, "Read in the logo image"); + diag "for the raw file the md5 hex is ". Digest::MD5::md5_hex($file) if $ENV{'TEST_VERBOSE'}; + + # Verify that the binary attachment is valid in the database + my $attachments = RT::Attachments->new($RT::SystemUser); + $attachments->Limit(FIELD => 'ContentType', VALUE => 'image/gif'); + my $txn_alias = $attachments->Join( + ALIAS1 => 'main', + FIELD1 => 'TransactionId', + TABLE2 => 'Transactions', + FIELD2 => 'id', + ); + $attachments->Limit( ALIAS => $txn_alias, FIELD => 'ObjectType', VALUE => 'RT::Ticket' ); + $attachments->Limit( ALIAS => $txn_alias, FIELD => 'ObjectId', VALUE => $id ); + is ($attachments->Count, 1, 'Found only one gif attached to the ticket'); + my $attachment = $attachments->First; + ok ($attachment->Id, 'loaded attachment object'); + my $acontent = $attachment->Content; + + diag "coming from the database, md5 hex is ".Digest::MD5::md5_hex($acontent) if $ENV{'TEST_VERBOSE'}; + is ($acontent, $file, 'The attachment isn\'t screwed up in the database.'); + + # Grab the binary attachment via the web ui + my $ua = new LWP::UserAgent; + my $full_url = "$url/Ticket/Attachment/". $attachment->TransactionId + ."/". $attachment->id. "/bplogo.gif?&user=root&pass=password"; + my $r = $ua->get( $full_url ); + + # Verify that the downloaded attachment is the same as what we uploaded. + is ($file, $r->content, 'The attachment isn\'t screwed up in download'); +} + +diag "Simple I18N testing" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rtemail\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of I18N ticket creation +Content-Type: text/plain; charset="utf-8" + +2 accented lines +\303\242\303\252\303\256\303\264\303\273 +\303\241\303\251\303\255\303\263\303\272 +bye +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "created ticket"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ". $tick->Id); + is ($tick->Id, $id, "correct ticket"); + is ($tick->Subject , 'This is a test of I18N ticket creation', "Created the ticket - ". $tick->Subject); + + my $unistring = "\303\241\303\251\303\255\303\263\303\272"; + Encode::_utf8_on($unistring); + is ( + $tick->Transactions->First->Content, + $tick->Transactions->First->Attachments->First->Content, + "Content is ". $tick->Transactions->First->Attachments->First->Content + ); + ok ( + $tick->Transactions->First->Content =~ /$unistring/i, + $tick->Id." appears to be unicode ". $tick->Transactions->First->Attachments->First->Id + ); +} + +diag "supposedly I18N fails on the second message sent in." if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +To: rtemail\@@{[RT->Config->Get('rtname')]} +Subject: This is a test of I18N ticket creation +Content-Type: text/plain; charset="utf-8" + +2 accented lines +\303\242\303\252\303\256\303\264\303\273 +\303\241\303\251\303\255\303\263\303\272 +bye +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "created ticket"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ". $tick->Id); + is ($tick->Id, $id, "correct ticket"); + is ($tick->Subject , 'This is a test of I18N ticket creation', "Created the ticket"); + + my $unistring = "\303\241\303\251\303\255\303\263\303\272"; + Encode::_utf8_on($unistring); + + ok ( + $tick->Transactions->First->Content =~ $unistring, + "It appears to be unicode - ". $tick->Transactions->First->Content + ); +} + +diag "check that mailgate doesn't suffer from empty Reply-To:" if $ENV{'TEST_VERBOSE'}; +{ + my $text = <<EOF; +From: root\@localhost +Reply-To: +To: rtemail\@@{[RT->Config->Get('rtname')]} +Subject: test +Content-Type: text/plain; charset="utf-8" + +test +EOF + my ($status, $id) = RT::Test->send_via_mailgate($text); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "created ticket"); + + my $tick = RT::Test->last_ticket; + isa_ok ($tick, 'RT::Ticket'); + ok ($tick->Id, "found ticket ". $tick->Id); + is ($tick->Id, $id, "correct ticket"); + + like $tick->RequestorAddresses, qr/root\@localhost/, 'correct requestor'; +} + + +my ($val,$msg) = $everyone_group->PrincipalObj->RevokeRight(Right => 'CreateTicket'); +ok ($val, $msg); + +SKIP: { +skip "Advanced mailgate actions require an unsafe configuration", 47 + unless RT->Config->Get('UnsafeEmailCommands'); + +# create new queue to be shure we don't mess with rights +use RT::Queue; +my $queue = RT::Queue->new($RT::SystemUser); +my ($qid) = $queue->Create( Name => 'ext-mailgate'); +ok( $qid, 'queue created for ext-mailgate tests' ); + +# {{{ Check take and resolve actions + +# create ticket that is owned by nobody +use RT::Ticket; +my $tick = RT::Ticket->new($RT::SystemUser); +my ($id) = $tick->Create( Queue => 'ext-mailgate', Subject => 'test'); +ok( $id, 'new ticket created' ); +is( $tick->Owner, $RT::Nobody->Id, 'owner of the new ticket is nobody' ); + +$! = 0; +ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take"), "Opened the mailgate - $!"); +print MAIL <<EOF; +From: root\@localhost +Subject: [@{[RT->Config->Get('rtname')]} \#$id] test + +EOF +close (MAIL); +is ($? >> 8, 0, "The mail gateway exited normally"); + +$tick = RT::Ticket->new($RT::SystemUser); +$tick->Load( $id ); +is( $tick->Id, $id, 'load correct ticket'); +is( $tick->OwnerObj->EmailAddress, 'root@localhost', 'successfuly take ticket via email'); + +# check that there is no text transactions writen +is( $tick->Transactions->Count, 2, 'no superfluous transactions'); + +my $status; +($status, $msg) = $tick->SetOwner( $RT::Nobody->Id, 'Force' ); +ok( $status, 'successfuly changed owner: '. ($msg||'') ); +is( $tick->Owner, $RT::Nobody->Id, 'set owner back to nobody'); + + +$! = 0; +ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take-correspond"), "Opened the mailgate - $@"); +print MAIL <<EOF; +From: root\@localhost +Subject: [@{[RT->Config->Get('rtname')]} \#$id] correspondence + +test +EOF +close (MAIL); +is ($? >> 8, 0, "The mail gateway exited normally"); + +DBIx::SearchBuilder::Record::Cachable->FlushCache; + +$tick = RT::Ticket->new($RT::SystemUser); +$tick->Load( $id ); +is( $tick->Id, $id, "load correct ticket #$id"); +is( $tick->OwnerObj->EmailAddress, 'root@localhost', 'successfuly take ticket via email'); +my $txns = $tick->Transactions; +$txns->Limit( FIELD => 'Type', VALUE => 'Correspond'); +$txns->OrderBy( FIELD => 'id', ORDER => 'DESC' ); +# +1 because of auto open +is( $tick->Transactions->Count, 6, 'no superfluous transactions'); +is( $txns->First->Subject, "[$RT::rtname \#$id] correspondence", 'successfuly add correspond within take via email' ); + +$! = 0; +ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action resolve"), "Opened the mailgate - $!"); +print MAIL <<EOF; +From: root\@localhost +Subject: [@{[RT->Config->Get('rtname')]} \#$id] test + +EOF +close (MAIL); +is ($? >> 8, 0, "The mail gateway exited normally"); + +DBIx::SearchBuilder::Record::Cachable->FlushCache; + +$tick = RT::Ticket->new($RT::SystemUser); +$tick->Load( $id ); +is( $tick->Id, $id, 'load correct ticket'); +is( $tick->Status, 'resolved', 'successfuly resolved ticket via email'); +is( $tick->Transactions->Count, 7, 'no superfluous transactions'); + +use RT::User; +my $user = RT::User->new( $RT::SystemUser ); +my ($uid) = $user->Create( Name => 'ext-mailgate', + EmailAddress => 'ext-mailgate@localhost', + Privileged => 1, + Password => 'qwe123', + ); +ok( $uid, 'user created for ext-mailgate tests' ); +ok( !$user->HasRight( Right => 'OwnTicket', Object => $queue ), "User can't own ticket" ); + +$tick = RT::Ticket->new($RT::SystemUser); +($id) = $tick->Create( Queue => $qid, Subject => 'test' ); +ok( $id, 'create new ticket' ); + +my $rtname = RT->Config->Get('rtname'); + +$! = 0; +ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take"), "Opened the mailgate - $!"); +print MAIL <<EOF; +From: ext-mailgate\@localhost +Subject: [$rtname \#$id] test + +EOF +close (MAIL); +is ( $? >> 8, 0, "mailgate exited normally" ); +DBIx::SearchBuilder::Record::Cachable->FlushCache; + +cmp_ok( $tick->Owner, '!=', $user->id, "we didn't change owner" ); + +($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'ReplyToTicket' ); +ok( $status, "successfuly granted right: $msg" ); +my $ace_id = $status; +ok( $user->HasRight( Right => 'ReplyToTicket', Object => $tick ), "User can reply to ticket" ); + +$! = 0; +ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action correspond-take"), "Opened the mailgate - $!"); +print MAIL <<EOF; +From: ext-mailgate\@localhost +Subject: [$rtname \#$id] test + +correspond-take +EOF +close (MAIL); +is ( $? >> 8, 0, "mailgate exited normally" ); +DBIx::SearchBuilder::Record::Cachable->FlushCache; + +cmp_ok( $tick->Owner, '!=', $user->id, "we didn't change owner" ); +is( $tick->Transactions->Count, 3, "one transactions added" ); + +$! = 0; +ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take-correspond"), "Opened the mailgate - $!"); +print MAIL <<EOF; +From: ext-mailgate\@localhost +Subject: [$rtname \#$id] test + +correspond-take +EOF +close (MAIL); +is ( $? >> 8, 0, "mailgate exited normally" ); +DBIx::SearchBuilder::Record::Cachable->FlushCache; + +cmp_ok( $tick->Owner, '!=', $user->id, "we didn't change owner" ); +is( $tick->Transactions->Count, 3, "no transactions added, user can't take ticket first" ); + +# revoke ReplyToTicket right +use RT::ACE; +my $ace = RT::ACE->new($RT::SystemUser); +$ace->Load( $ace_id ); +$ace->Delete; +my $acl = RT::ACL->new($RT::SystemUser); +$acl->Limit( FIELD => 'RightName', VALUE => 'ReplyToTicket' ); +$acl->LimitToObject( $RT::System ); +while( my $ace = $acl->Next ) { + $ace->Delete; +} + +ok( !$user->HasRight( Right => 'ReplyToTicket', Object => $tick ), "User can't reply to ticket any more" ); + + +my $group = RT::Group->new( $RT::SystemUser ); +ok( $group->LoadQueueRoleGroup( Queue => $qid, Type=> 'Owner' ), "load queue owners role group" ); +$ace = RT::ACE->new( $RT::SystemUser ); +($ace_id, $msg) = $group->PrincipalObj->GrantRight( Right => 'ReplyToTicket', Object => $queue ); +ok( $ace_id, "Granted queue owners role group with ReplyToTicket right" ); + +($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'OwnTicket' ); +ok( $status, "successfuly granted right: $msg" ); +($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'TakeTicket' ); +ok( $status, "successfuly granted right: $msg" ); + +$! = 0; +ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take-correspond"), "Opened the mailgate - $!"); +print MAIL <<EOF; +From: ext-mailgate\@localhost +Subject: [$rtname \#$id] test + +take-correspond with reply right granted to owner role +EOF +close (MAIL); +is ( $? >> 8, 0, "mailgate exited normally" ); +DBIx::SearchBuilder::Record::Cachable->FlushCache; + +$tick->Load( $id ); +is( $tick->Owner, $user->id, "we changed owner" ); +ok( $user->HasRight( Right => 'ReplyToTicket', Object => $tick ), "owner can reply to ticket" ); +is( $tick->Transactions->Count, 5, "transactions added" ); + + +# }}} +}; + + +1; + diff --git a/rt/t/mail/gnupg-bad.t b/rt/t/mail/gnupg-bad.t new file mode 100644 index 000000000..2d8e03575 --- /dev/null +++ b/rt/t/mail/gnupg-bad.t @@ -0,0 +1,58 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => 6; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use Cwd 'getcwd'; + +my $homedir = RT::Test::get_abs_relocatable_dir(File::Spec->updir(), + qw(data gnupg keyrings)); + +RT->Config->Set( 'GnuPG', + Enable => 1, + OutgoingMessagesFormat => 'RFC' ); + +RT->Config->Set( 'GnuPGOptions', + homedir => $homedir, + passphrase => 'test', + 'no-permission-warning' => undef); + +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + +my ($baseurl, $m) = RT::Test->started_ok; + +$m->get( $baseurl."?user=root;pass=password" ); +$m->content_like(qr/Logout/, 'we did log in'); +$m->get( $baseurl.'/Admin/Queues/'); +$m->follow_link_ok( {text => 'General'} ); +$m->submit_form( form_number => 3, + fields => { CorrespondAddress => 'rt@example.com' } ); +$m->content_like(qr/rt\@example.com.* - never/, 'has key info.'); + +ok(my $user = RT::User->new($RT::SystemUser)); +ok($user->Load('root'), "Loaded user 'root'"); +$user->SetEmailAddress('rt@example.com'); + +if (0) { + # XXX: need to generate these mails + diag "no signature" if $ENV{TEST_VERBOSE}; + diag "no encryption on encrypted queue" if $ENV{TEST_VERBOSE}; + diag "mismatched signature" if $ENV{TEST_VERBOSE}; + diag "unknown public key" if $ENV{TEST_VERBOSE}; + diag "unknown private key" if $ENV{TEST_VERBOSE}; + diag "signer != sender" if $ENV{TEST_VERBOSE}; + diag "encryption to user whose pubkey is not signed" if $ENV{TEST_VERBOSE}; + diag "no encryption of attachment on encrypted queue" if $ENV{TEST_VERBOSE}; + diag "no signature of attachment" if $ENV{TEST_VERBOSE}; + diag "revoked key" if $ENV{TEST_VERBOSE}; + diag "expired key" if $ENV{TEST_VERBOSE}; + diag "unknown algorithm" if $ENV{TEST_VERBOSE}; +} + diff --git a/rt/t/mail/gnupg-incoming.t b/rt/t/mail/gnupg-incoming.t new file mode 100644 index 000000000..ec313330a --- /dev/null +++ b/rt/t/mail/gnupg-incoming.t @@ -0,0 +1,320 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => 39; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use File::Temp; +use Cwd 'getcwd'; +use String::ShellQuote 'shell_quote'; +use IPC::Run3 'run3'; + +my $homedir = RT::Test::get_abs_relocatable_dir(File::Spec->updir(), + qw(data gnupg keyrings)); + +# catch any outgoing emails +RT::Test->set_mail_catcher; + +RT->Config->Set( 'GnuPG', + Enable => 1, + OutgoingMessagesFormat => 'RFC' ); + +RT->Config->Set( 'GnuPGOptions', + homedir => $homedir, + 'no-permission-warning' => undef); + +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + +my ($baseurl, $m) = RT::Test->started_ok; + +# configure key for General queue +$m->get( $baseurl."?user=root;pass=password" ); +$m->content_like(qr/Logout/, 'we did log in'); +$m->get( $baseurl.'/Admin/Queues/'); +$m->follow_link_ok( {text => 'General'} ); +$m->submit_form( form_number => 3, + fields => { CorrespondAddress => 'general@example.com' } ); +$m->content_like(qr/general\@example.com.* - never/, 'has key info.'); + +ok(my $user = RT::User->new($RT::SystemUser)); +ok($user->Load('root'), "Loaded user 'root'"); +$user->SetEmailAddress('recipient@example.com'); + +# test simple mail. supposedly this should fail when +# 1. the queue requires signature +# 2. the from is not what the key is associated with +my $mail = RT::Test->open_mailgate_ok($baseurl); +print $mail <<EOF; +From: recipient\@example.com +To: general\@$RT::rtname +Subject: This is a test of new ticket creation as root + +Blah! +Foob! +EOF +RT::Test->close_mailgate_ok($mail); + +{ + my $tick = RT::Test->last_ticket; + is( $tick->Subject, + 'This is a test of new ticket creation as root', + "Created the ticket" + ); + my $txn = $tick->Transactions->First; + like( + $txn->Attachments->First->Headers, + qr/^X-RT-Incoming-Encryption: Not encrypted/m, + 'recorded incoming mail that is not encrypted' + ); + like( $txn->Attachments->First->Content, qr'Blah'); +} + +# test for signed mail +my $buf = ''; + +run3( + shell_quote( + qw(gpg --armor --sign), + '--default-key' => 'recipient@example.com', + '--homedir' => $homedir, + '--passphrase' => 'recipient', + ), + \"fnord\r\n", + \$buf, + \*STDOUT +); + +$mail = RT::Test->open_mailgate_ok($baseurl); +print $mail <<"EOF"; +From: recipient\@example.com +To: general\@$RT::rtname +Subject: signed message for queue + +$buf +EOF +RT::Test->close_mailgate_ok($mail); + +{ + my $tick = RT::Test->last_ticket; + is( $tick->Subject, 'signed message for queue', + "Created the ticket" + ); + + my $txn = $tick->Transactions->First; + my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef}; + + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Not encrypted', + 'recorded incoming mail that is encrypted' + ); + # test for some kind of PGP-Signed-By: Header + like( $attach->Content, qr'fnord'); +} + +# test for clear-signed mail +$buf = ''; + +run3( + shell_quote( + qw(gpg --armor --sign --clearsign), + '--default-key' => 'recipient@example.com', + '--homedir' => $homedir, + '--passphrase' => 'recipient', + ), + \"clearfnord\r\n", + \$buf, + \*STDOUT +); + +$mail = RT::Test->open_mailgate_ok($baseurl); +print $mail <<"EOF"; +From: recipient\@example.com +To: general\@$RT::rtname +Subject: signed message for queue + +$buf +EOF +RT::Test->close_mailgate_ok($mail); + +{ + my $tick = RT::Test->last_ticket; + is( $tick->Subject, 'signed message for queue', + "Created the ticket" + ); + + my $txn = $tick->Transactions->First; + my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef}; + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Not encrypted', + 'recorded incoming mail that is encrypted' + ); + # test for some kind of PGP-Signed-By: Header + like( $attach->Content, qr'clearfnord'); +} + +# test for signed and encrypted mail +$buf = ''; + +run3( + shell_quote( + qw(gpg --encrypt --armor --sign), + '--recipient' => 'general@example.com', + '--default-key' => 'recipient@example.com', + '--homedir' => $homedir, + '--passphrase' => 'recipient', + ), + \"orzzzzzz\r\n", + \$buf, + \*STDOUT +); + +$mail = RT::Test->open_mailgate_ok($baseurl); +print $mail <<"EOF"; +From: recipient\@example.com +To: general\@$RT::rtname +Subject: Encrypted message for queue + +$buf +EOF +RT::Test->close_mailgate_ok($mail); + +{ + my $tick = RT::Test->last_ticket; + is( $tick->Subject, 'Encrypted message for queue', + "Created the ticket" + ); + + my $txn = $tick->Transactions->First; + my ($msg, $attach, $orig) = @{$txn->Attachments->ItemsArrayRef}; + + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Success', + 'recorded incoming mail that is encrypted' + ); + is( $msg->GetHeader('X-RT-Privacy'), + 'PGP', + 'recorded incoming mail that is encrypted' + ); + like( $attach->Content, qr'orz'); + + is( $orig->GetHeader('Content-Type'), 'application/x-rt-original-message'); + ok(index($orig->Content, $buf) != -1, 'found original msg'); +} + +# test for signed mail by other key +$buf = ''; + +run3( + shell_quote( + qw(gpg --armor --sign), + '--default-key' => 'rt@example.com', + '--homedir' => $homedir, + '--passphrase' => 'test', + ), + \"alright\r\n", + \$buf, + \*STDOUT +); + +$mail = RT::Test->open_mailgate_ok($baseurl); +print $mail <<"EOF"; +From: recipient\@example.com +To: general\@$RT::rtname +Subject: signed message for queue + +$buf +EOF +RT::Test->close_mailgate_ok($mail); + +{ + my $tick = RT::Test->last_ticket; + my $txn = $tick->Transactions->First; + my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef}; + # XXX: in this case, which credential should we be using? + is( $msg->GetHeader('X-RT-Incoming-Signature'), + 'Test User <rt@example.com>', + 'recorded incoming mail signed by others' + ); +} + +# test for encrypted mail with key not associated to the queue +$buf = ''; + +run3( + shell_quote( + qw(gpg --armor --encrypt), + '--recipient' => 'random@localhost', + '--homedir' => $homedir, + ), + \"should not be there either\r\n", + \$buf, + \*STDOUT +); + +$mail = RT::Test->open_mailgate_ok($baseurl); +print $mail <<"EOF"; +From: recipient\@example.com +To: general\@$RT::rtname +Subject: encrypted message for queue + +$buf +EOF +RT::Test->close_mailgate_ok($mail); + +{ + my $tick = RT::Test->last_ticket; + my $txn = $tick->Transactions->First; + my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef}; + + TODO: + { + local $TODO = "this test requires keys associated with queues"; + unlike( $attach->Content, qr'should not be there either'); + } +} + +# test for badly encrypted mail +{ +$buf = ''; + +run3( + shell_quote( + qw(gpg --armor --encrypt), + '--recipient' => 'rt@example.com', + '--homedir' => $homedir, + ), + \"really should not be there either\r\n", + \$buf, + \*STDOUT +); + +$buf =~ s/PGP MESSAGE/SCREWED UP/g; + +RT::Test->fetch_caught_mails; + +$mail = RT::Test->open_mailgate_ok($baseurl); +print $mail <<"EOF"; +From: recipient\@example.com +To: general\@$RT::rtname +Subject: encrypted message for queue + +$buf +EOF +RT::Test->close_mailgate_ok($mail); +my @mail = RT::Test->fetch_caught_mails; +is(@mail, 1, 'caught outgoing mail.'); +} + +{ + my $tick = RT::Test->last_ticket; + my $txn = $tick->Transactions->First; + my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef}; + unlike( ($attach ? $attach->Content : ''), qr'really should not be there either'); +} + diff --git a/rt/t/mail/gnupg-realmail.t b/rt/t/mail/gnupg-realmail.t new file mode 100644 index 000000000..de1d95815 --- /dev/null +++ b/rt/t/mail/gnupg-realmail.t @@ -0,0 +1,184 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => 197; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use Digest::MD5 qw(md5_hex); + +use File::Temp qw(tempdir); +my $homedir = tempdir( CLEANUP => 1 ); + +RT->Config->Set( 'GnuPG', + Enable => 1, + OutgoingMessagesFormat => 'RFC' ); + +RT->Config->Set( 'GnuPGOptions', + homedir => $homedir, + passphrase => 'rt-test', + 'no-permission-warning' => undef); + +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + +RT::Test->import_gnupg_key('rt-recipient@example.com'); +RT::Test->import_gnupg_key('rt-test@example.com', 'public'); + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'we did log in'; +$m->get_ok( '/Admin/Queues/'); +$m->follow_link_ok( {text => 'General'} ); +$m->submit_form( form_number => 3, + fields => { CorrespondAddress => 'rt-recipient@example.com' } ); +$m->content_like(qr/rt-recipient\@example.com.* - never/, 'has key info.'); + +diag "load Everyone group" if $ENV{'TEST_VERBOSE'}; +my $everyone; +{ + $everyone = RT::Group->new( $RT::SystemUser ); + $everyone->LoadSystemInternalGroup('Everyone'); + ok $everyone->id, "loaded 'everyone' group"; +} + +RT::Test->set_rights( + Principal => $everyone, + Right => ['CreateTicket'], +); + + +my $eid = 0; +for my $usage (qw/signed encrypted signed&encrypted/) { + for my $format (qw/MIME inline/) { + for my $attachment (qw/plain text-attachment binary-attachment/) { + ++$eid; + diag "Email $eid: $usage, $attachment email with $format format" if $ENV{TEST_VERBOSE}; + eval { email_ok($eid, $usage, $format, $attachment) }; + } + } +} + +$eid = 18; +{ + my ($usage, $format, $attachment) = ('signed', 'inline', 'plain'); + ++$eid; + diag "Email $eid: $usage, $attachment email with $format format" if $ENV{TEST_VERBOSE}; + eval { email_ok($eid, $usage, $format, $attachment) }; +} + +sub email_ok { + my ($eid, $usage, $format, $attachment) = @_; + diag "email_ok $eid: $usage, $format, $attachment" if $ENV{'TEST_VERBOSE'}; + + my $emaildatadir = RT::Test::get_relocatable_dir(File::Spec->updir(), + qw(data gnupg emails)); + my ($file) = glob("$emaildatadir/$eid-*"); + my $mail = RT::Test->file_content($file); + + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "$eid: The mail gateway exited normally"); + ok ($id, "$eid: got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "$eid: loaded ticket #$id"); + + is ($tick->Subject, + "Test Email ID:$eid", + "$eid: Created the ticket" + ); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is( $msg->GetHeader('X-RT-Privacy'), + 'PGP', + "$eid: recorded incoming mail that is encrypted" + ); + + if ($usage =~ /encrypted/) { + if ( $format eq 'MIME' || $attachment eq 'plain' ) { + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Success', + "$eid: recorded incoming mail that is encrypted" + ); + } else { + is( $attachments[0]->GetHeader('X-RT-Incoming-Encryption'), + 'Success', + "$eid: recorded incoming mail that is encrypted" + ); + is( $attachments[1]->GetHeader('X-RT-Incoming-Encryption'), + 'Success', + "$eid: recorded incoming mail that is encrypted" + ); + } + like( $attachments[0]->Content, qr/ID:$eid/, + "$eid: incoming mail did NOT have original body" + ); + } + else { + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Not encrypted', + "$eid: recorded incoming mail that is not encrypted" + ); + like( $msg->Content || $attachments[0]->Content, qr/ID:$eid/, + "$eid: got original content" + ); + } + + if ($usage =~ /signed/) { +# XXX: FIXME: TODO: 6-signed-inline-with-attachment should be re-generated as it's actually RFC format + if ( $format eq 'MIME' || $attachment eq 'plain' || ($format eq 'inline' && $attachment =~ /binary/ && $usage !~ /encrypted/) ) { + is( $msg->GetHeader('X-RT-Incoming-Signature'), + 'RT Test <rt-test@example.com>', + "$eid: recorded incoming mail that is signed" + ); + } + else { + is( $attachments[0]->GetHeader('X-RT-Incoming-Signature'), + 'RT Test <rt-test@example.com>', + "$eid: recorded incoming mail that is signed" + ); + is( $attachments[1]->GetHeader('X-RT-Incoming-Signature'), + 'RT Test <rt-test@example.com>', + "$eid: recorded incoming mail that is signed" + ); + } + } + else { + is( $msg->GetHeader('X-RT-Incoming-Signature'), + undef, + "$eid: recorded incoming mail that is not signed" + ); + } + + if ($attachment =~ /attachment/) { + # signed messages should sign each attachment too + if ($usage =~ /signed/) { + my $sig = pop @attachments; + ok ($sig->Id, "$eid: loaded attachment.sig object"); + my $acontent = $sig->Content; + } + + my ($a) = grep $_->Filename, @attachments; + ok ($a && $a->Id, "$eid: found attachment with filename"); + + my $acontent = $a->Content; + if ($attachment =~ /binary/) + { + is(md5_hex($acontent), '1e35f1aa90c98ca2bab85c26ae3e1ba7', "$eid: The binary attachment's md5sum matches"); + } + else + { + like($acontent, qr/zanzibar/, "$eid: The attachment isn't screwed up in the database."); + } + + } + + return 0; +} + diff --git a/rt/t/mail/gnupg-reverification.t b/rt/t/mail/gnupg-reverification.t new file mode 100644 index 000000000..f116d9380 --- /dev/null +++ b/rt/t/mail/gnupg-reverification.t @@ -0,0 +1,92 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => 120; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use File::Temp qw(tempdir); +my $homedir = tempdir( CLEANUP => 1 ); + +RT->Config->Set( 'GnuPG', + Enable => 1, + OutgoingMessagesFormat => 'RFC' ); + +RT->Config->Set( 'GnuPGOptions', + homedir => $homedir, + passphrase => 'rt-test', + 'no-permission-warning' => undef); + +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + + +diag "load Everyone group" if $ENV{'TEST_VERBOSE'}; +my $everyone; +{ + $everyone = RT::Group->new( $RT::SystemUser ); + $everyone->LoadSystemInternalGroup('Everyone'); + ok $everyone->id, "loaded 'everyone' group"; +} + +RT::Test->set_rights( + Principal => $everyone, + Right => ['CreateTicket'], +); + + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'we get log in'; + +RT::Test->import_gnupg_key('rt-recipient@example.com'); + +my @ticket_ids; + +my $emaildatadir = RT::Test::get_relocatable_dir(File::Spec->updir(), + qw(data gnupg emails)); +my @files = glob("$emaildatadir/*-signed-*"); +foreach my $file ( @files ) { + diag "testing $file" if $ENV{'TEST_VERBOSE'}; + + my ($eid) = ($file =~ m{(\d+)[^/\\]+$}); + ok $eid, 'figured id of a file'; + + my $email_content = RT::Test->file_content( $file ); + ok $email_content, "$eid: got content of email"; + + my ($status, $id) = RT::Test->send_via_mailgate( $email_content ); + is $status >> 8, 0, "$eid: the mail gateway exited normally"; + ok $id, "$eid: got id of a newly created ticket - $id"; + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $id ); + ok $ticket->id, "$eid: loaded ticket #$id"; + is $ticket->Subject, "Test Email ID:$eid", "$eid: correct subject"; + + $m->goto_ticket( $id ); + $m->content_like( + qr/Not possible to check the signature, the reason is missing public key/is, + "$eid: signature is not verified", + ); + $m->content_like(qr/This is .*ID:$eid/ims, "$eid: content is there and message is decrypted"); + + push @ticket_ids, $id; +} + +diag "import key into keyring" if $ENV{'TEST_VERBOSE'}; +RT::Test->import_gnupg_key('rt-test@example.com', 'public'); + +foreach my $id ( @ticket_ids ) { + diag "testing ticket #$id" if $ENV{'TEST_VERBOSE'}; + + $m->goto_ticket( $id ); + $m->content_like( + qr/The signature is good/is, + "signature is re-verified and now good", + ); +} + diff --git a/rt/t/mail/mime_decoding.t b/rt/t/mail/mime_decoding.t new file mode 100644 index 000000000..8257aee80 --- /dev/null +++ b/rt/t/mail/mime_decoding.t @@ -0,0 +1,59 @@ +#!/usr/bin/perl +use strict; +use warnings; +use RT::Test nodata => 1, tests => 6; + +use_ok('RT::I18N'); + +diag q{'=' char in a leading part before an encoded part} if $ENV{TEST_VERBOSE}; +{ + my $str = 'key="plain"; key="=?UTF-8?B?0LzQvtC5X9GE0LDQudC7LmJpbg==?="'; + is( + RT::I18N::DecodeMIMEWordsToUTF8($str), + 'key="plain"; key="мой_файл.bin"', + "right decoding" + ); +} + +diag q{not compliant with standards, but MUAs send such field when attachment has non-ascii in name} + if $ENV{TEST_VERBOSE}; +{ + my $str = 'attachment; filename="=?UTF-8?B?0LzQvtC5X9GE0LDQudC7LmJpbg==?="'; + is( + RT::I18N::DecodeMIMEWordsToUTF8($str), + 'attachment; filename="мой_файл.bin"', + "right decoding" + ); +} + +diag q{'=' char in a trailing part after an encoded part} if $ENV{TEST_VERBOSE}; +{ + my $str = 'attachment; filename="=?UTF-8?B?0LzQvtC5X9GE0LDQudC7LmJpbg==?="; some_prop="value"'; + is( + RT::I18N::DecodeMIMEWordsToUTF8($str), + 'attachment; filename="мой_файл.bin"; some_prop="value"', + "right decoding" + ); +} + +diag q{regression test for #5248 from rt3.fsck.com} if $ENV{TEST_VERBOSE}; +{ + my $str = qq{Subject: =?ISO-8859-1?Q?Re=3A_=5BXXXXXX=23269=5D_=5BComment=5D_Frag?=} + . qq{\n =?ISO-8859-1?Q?e_zu_XXXXXX--xxxxxx_/_Xxxxx=FCxxxxxxxxxx?=}; + is( + RT::I18N::DecodeMIMEWordsToUTF8($str), + qq{Subject: Re: [XXXXXX#269] [Comment] Frage zu XXXXXX--xxxxxx / Xxxxxüxxxxxxxxxx}, + "right decoding" + ); +} + +diag q{newline and encoded file name} if $ENV{TEST_VERBOSE}; +{ + my $str = qq{application/vnd.ms-powerpoint;\n\tname="=?ISO-8859-1?Q?Main_presentation.ppt?="}; + is( + RT::I18N::DecodeMIMEWordsToUTF8($str), + qq{application/vnd.ms-powerpoint;\tname="Main presentation.ppt"}, + "right decoding" + ); +} + diff --git a/rt/t/mail/sendmail.t b/rt/t/mail/sendmail.t new file mode 100644 index 000000000..1f97bbb9f --- /dev/null +++ b/rt/t/mail/sendmail.t @@ -0,0 +1,538 @@ +#!/usr/bin/perl -w + +use strict; +use File::Spec (); + +use RT::Test tests => 137; + +use RT::EmailParser; +use RT::Tickets; +use RT::Action::SendEmail; + +my @_outgoing_messages; +my @scrips_fired; + +#We're not testing acls here. +my $everyone = RT::Group->new($RT::SystemUser); +$everyone->LoadSystemInternalGroup('Everyone'); +$everyone->PrincipalObj->GrantRight( Right =>'SuperUser' ); + + +is (__PACKAGE__, 'main', "We're operating in the main package"); + +{ + no warnings qw/redefine/; + sub RT::Action::SendEmail::SendMessage { + my $self = shift; + my $MIME = shift; + + main::_fired_scrip($self->ScripObj); + main::is(ref($MIME) , 'MIME::Entity', "hey, look. it's a mime entity"); + } +} + +# some utils +sub first_txn { return $_[0]->Transactions->First } +sub first_attach { return first_txn($_[0])->Attachments->First } + +sub count_txns { return $_[0]->Transactions->Count } +sub count_attachs { return first_txn($_[0])->Attachments->Count } + +# instrument SendEmail to pass us what it's about to send. +# create a regular ticket + +my $parser = RT::EmailParser->new(); + +# Let's test to make sure a multipart/report is processed correctly +my $multipart_report_email = RT::Test::get_relocatable_file('multipart-report', + (File::Spec->updir(), 'data', 'emails')); +my $content = RT::Test->file_content($multipart_report_email); +# be as much like the mail gateway as possible. +use RT::Interface::Email; +my %args = (message => $content, queue => 1, action => 'correspond'); +my ($status, $msg) = RT::Interface::Email::Gateway(\%args); +ok($status, "successfuly used Email::Gateway interface") or diag("error: $msg"); +my $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); +my $tick= $tickets->First(); +isa_ok($tick, "RT::Ticket", "got a ticket object"); +ok ($tick->Id, "found ticket ".$tick->Id); +like (first_txn($tick)->Content , qr/The original message was received/, "It's the bounce"); + + +# make sure it fires scrips. +is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation"); + +undef @scrips_fired; + + + + +$parser->ParseMIMEEntityFromScalar('From: root@localhost +To: rt@example.com +Subject: This is a test of new ticket creation as an unknown user + +Blah! +Foob!'); + + +use Data::Dumper; + +my $ticket = RT::Ticket->new($RT::SystemUser); +my ($id, undef, $create_msg ) = $ticket->Create(Requestor => ['root@localhost'], Queue => 'general', Subject => 'I18NTest', MIMEObj => $parser->Entity); +ok ($id,$create_msg); +$tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); + $tick = $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); +is ($tick->Subject , 'I18NTest', "failed to create the new ticket from an unprivileged account"); + +# make sure it fires scrips. +is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation"); +# make sure it sends an autoreply +# make sure it sends a notification to adminccs + + +# we need to swap out SendMessage to test the new things we care about; +&utf8_redef_sendmessage; + +# create an iso 8859-1 ticket +@scrips_fired = (); + +my $iso_8859_1_ticket_email = RT::Test::get_relocatable_file( + 'new-ticket-from-iso-8859-1', (File::Spec->updir(), 'data', 'emails')); +$content = RT::Test->file_content($iso_8859_1_ticket_email); + + + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. +use RT::Interface::Email; + + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); + $tick = $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); + +like (first_txn($tick)->Content , qr/H\x{e5}vard/, "It's signed by havard. yay"); + + +# make sure it fires scrips. +is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation"); +# make sure it sends an autoreply + + +# make sure it sends a notification to adminccs + +# If we correspond, does it do the right thing to the outbound messages? + +$parser->ParseMIMEEntityFromScalar($content); + ($id, $msg) = $tick->Comment(MIMEObj => $parser->Entity); +ok ($id, $msg); + +$parser->ParseMIMEEntityFromScalar($content); +($id, $msg) = $tick->Correspond(MIMEObj => $parser->Entity); +ok ($id, $msg); + + + + + +# we need to swap out SendMessage to test the new things we care about; +&iso8859_redef_sendmessage; +RT->Config->Set( EmailOutputEncoding => 'iso-8859-1' ); +# create an iso 8859-1 ticket +@scrips_fired = (); + + $content = RT::Test->file_content($iso_8859_1_ticket_email); +# be as much like the mail gateway as possible. +use RT::Interface::Email; + + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); +$tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); + $tick = $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); + +like (first_txn($tick)->Content , qr/H\x{e5}vard/, "It's signed by havard. yay"); + + +# make sure it fires scrips. +is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation"); +# make sure it sends an autoreply + + +# make sure it sends a notification to adminccs + + +# If we correspond, does it do the right thing to the outbound messages? + +$parser->ParseMIMEEntityFromScalar($content); + ($id, $msg) = $tick->Comment(MIMEObj => $parser->Entity); +ok ($id, $msg); + +$parser->ParseMIMEEntityFromScalar($content); +($id, $msg) = $tick->Correspond(MIMEObj => $parser->Entity); +ok ($id, $msg); + + +sub _fired_scrip { + my $scrip = shift; + push @scrips_fired, $scrip; +} + +sub utf8_redef_sendmessage { + no warnings qw/redefine/; + eval ' + sub RT::Action::SendEmail::SendMessage { + my $self = shift; + my $MIME = shift; + + my $scrip = $self->ScripObj->id; + ok(1, $self->ScripObj->ConditionObj->Name . " ".$self->ScripObj->ActionObj->Name); + main::_fired_scrip($self->ScripObj); + $MIME->make_singlepart; + main::is( ref($MIME) , \'MIME::Entity\', + "hey, look. it\'s a mime entity" ); + main::is( ref( $MIME->head ) , \'MIME::Head\', + "its mime header is a mime header. yay" ); + main::like( $MIME->head->get(\'Content-Type\') , qr/utf-8/, + "Its content type is utf-8" ); + my $message_as_string = $MIME->bodyhandle->as_string(); + use Encode; + $message_as_string = Encode::decode_utf8($message_as_string); + main::like( + $message_as_string , qr/H\x{e5}vard/, +"The message\'s content contains havard\'s name. this will fail if it\'s not utf8 out"); + + }'; +} + +sub iso8859_redef_sendmessage { + no warnings qw/redefine/; + eval ' + sub RT::Action::SendEmail::SendMessage { + my $self = shift; + my $MIME = shift; + + my $scrip = $self->ScripObj->id; + ok(1, $self->ScripObj->ConditionObj->Name . " ".$self->ScripObj->ActionObj->Name); + main::_fired_scrip($self->ScripObj); + $MIME->make_singlepart; + main::is( ref($MIME) , \'MIME::Entity\', + "hey, look. it\'s a mime entity" ); + main::is( ref( $MIME->head ) , \'MIME::Head\', + "its mime header is a mime header. yay" ); + main::like( $MIME->head->get(\'Content-Type\') , qr/iso-8859-1/, + "Its content type is iso-8859-1 - " . $MIME->head->get("Content-Type") ); + my $message_as_string = $MIME->bodyhandle->as_string(); + use Encode; + $message_as_string = Encode::decode("iso-8859-1",$message_as_string); + main::like( + $message_as_string , qr/H\x{e5}vard/, "The message\'s content contains havard\'s name. this will fail if it\'s not utf8 out"); + + }'; +} + +# {{{ test a multipart alternative containing a text-html part with an umlaut + + my $alt_umlaut_email = RT::Test::get_relocatable_file( + 'multipart-alternative-with-umlaut', (File::Spec->updir(), 'data', 'emails')); + $content = RT::Test->file_content($alt_umlaut_email); + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. +{ + no warnings qw/redefine/; + local *RT::Action::SendEmail::SendMessage = sub { return 1}; + + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + # TODO: following 5 lines should replaced by get_latest_ticket_ok() + $tickets = RT::Tickets->new($RT::SystemUser); + $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); + $tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); + $tick = $tickets->First(); + + ok ($tick->Id, "found ticket ".$tick->Id); + + like (first_txn($tick)->Content , qr/causes Error/, "We recorded the content right as text-plain"); + is (count_attachs($tick) , 3 , "Has three attachments, presumably a text-plain, a text-html and a multipart alternative"); + +} + +# }}} + +# {{{ test a text-html message with an umlaut + my $text_html_email = RT::Test::get_relocatable_file('text-html-with-umlaut', + (File::Spec->updir(), 'data', 'emails')); + $content = RT::Test->file_content($text_html_email); + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. +&text_html_redef_sendmessage; + + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); + $tick = $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); + +like (first_attach($tick)->Content , qr/causes Error/, "We recorded the content as containing 'causes error'") or diag( first_attach($tick)->Content ); +like (first_attach($tick)->ContentType , qr/text\/html/, "We recorded the content as text/html"); +is (count_attachs($tick), 1 , "Has one attachment, presumably a text-html and a multipart alternative"); + +sub text_html_redef_sendmessage { + no warnings qw/redefine/; + eval 'sub RT::Action::SendEmail::SendMessage { + my $self = shift; + my $MIME = shift; + return (1) unless ($self->ScripObj->ScripActionObj->Name eq "Notify AdminCcs" ); + is ($MIME->parts, 0, "generated correspondence mime entity + does not have parts"); + is ($MIME->head->mime_type , "text/plain", "The mime type is a plain"); + }'; +} + +# }}} + +# {{{ test a text-html message with russian characters + my $russian_email = RT::Test::get_relocatable_file('text-html-in-russian', + (File::Spec->updir(), 'data', 'emails')); + $content = RT::Test->file_content($russian_email); + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. +&text_html_redef_sendmessage; + + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); + $tick = $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); + +like (first_attach($tick)->ContentType , qr/text\/html/, "We recorded the content right as text-html"); + +is (count_attachs($tick) ,1 , "Has one attachment, presumably a text-html and a multipart alternative"); + +# }}} + +# {{{ test a message containing a russian subject and NO content type + +RT->Config->Set( EmailInputEncodings => 'koi8-r', RT->Config->Get('EmailInputEncodings') ); +RT->Config->Set( EmailOutputEncoding => 'koi8-r' ); +my $russian_subject_email = RT::Test::get_relocatable_file( + 'russian-subject-no-content-type', (File::Spec->updir(), 'data', 'emails')); +$content = RT::Test->file_content($russian_subject_email); + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. +&text_plain_russian_redef_sendmessage; + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); +$tick= $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); + +like (first_attach($tick)->ContentType , qr/text\/plain/, "We recorded the content type right"); +is (count_attachs($tick) ,1 , "Has one attachment, presumably a text-plain"); +is ($tick->Subject, "\x{442}\x{435}\x{441}\x{442} \x{442}\x{435}\x{441}\x{442}", "Recorded the subject right"); +sub text_plain_russian_redef_sendmessage { + no warnings qw/redefine/; + eval 'sub RT::Action::SendEmail::SendMessage { + my $self = shift; + my $MIME = shift; + return (1) unless ($self->ScripObj->ScripActionObj->Name eq "Notify AdminCcs" ); + is ($MIME->head->mime_type , "text/plain", "The only part is text/plain "); + my $subject = $MIME->head->get("subject"); + chomp($subject); + #is( $subject , /^=\?KOI8-R\?B\?W2V4YW1wbGUuY39tICM3XSDUxdPUINTF09Q=\?=/ , "The $subject is encoded correctly"); + }; + '; +} + +my @input_encodings = RT->Config->Get( 'EmailInputEncodings' ); +shift @input_encodings; +RT->Config->Set(EmailInputEncodings => @input_encodings ); +RT->Config->Set(EmailOutputEncoding => 'utf-8'); +# }}} + + +# {{{ test a message containing a nested RFC 822 message + +my $nested_rfc822_email = RT::Test::get_relocatable_file('nested-rfc-822', + (File::Spec->updir(), 'data', 'emails')); +$content = RT::Test->file_content($nested_rfc822_email); +ok ($content, "Loaded nested-rfc-822 to test"); + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. +&text_plain_nested_redef_sendmessage; + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); +$tick= $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); +is ($tick->Subject, "[Jonas Liljegren] Re: [Para] Niv\x{e5}er?"); +like (first_attach($tick)->ContentType , qr/multipart\/mixed/, "We recorded the content type right"); +is (count_attachs($tick) , 5 , "Has one attachment, presumably a text-plain and a message RFC 822 and another plain"); +sub text_plain_nested_redef_sendmessage { + no warnings qw/redefine/; + eval 'sub RT::Action::SendEmail::SendMessage { + my $self = shift; + my $MIME = shift; + return (1) unless ($self->ScripObj->ScripActionObj->Name eq "Notify AdminCcs" ); + is ($MIME->head->mime_type , "multipart/mixed", "It is a mixed multipart"); + my $subject = $MIME->head->get("subject"); + $subject = MIME::Base64::decode_base64( $subject); + chomp($subject); + # TODO, why does this test fail + #ok($subject =~ qr{Niv\x{e5}er}, "The subject matches the word - $subject"); + 1; + }'; +} + +# }}} + + +# {{{ test a multipart alternative containing a uuencoded mesage generated by lotus notes + + my $uuencoded_email = RT::Test::get_relocatable_file('notes-uuencoded', + (File::Spec->updir(), 'data', 'emails')); + $content = RT::Test->file_content($uuencoded_email); + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. +{ + no warnings qw/redefine/; + local *RT::Action::SendEmail::SendMessage = sub { return 1}; + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); + $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); + $tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); + $tick= $tickets->First(); + ok ($tick->Id, "found ticket ".$tick->Id); + + like (first_txn($tick)->Content , qr/from Lotus Notes/, "We recorded the content right"); + is (count_attachs($tick) , 3 , "Has three attachments"); +} + +# }}} + +# {{{ test a multipart that crashes the file-based mime-parser works + + my $crashes_file_based_parser_email = RT::Test::get_relocatable_file( + 'crashes-file-based-parser', (File::Spec->updir(), 'data', 'emails')); + $content = RT::Test->file_content($crashes_file_based_parser_email); + +$parser->ParseMIMEEntityFromScalar($content); + + +# be as much like the mail gateway as possible. + +no warnings qw/redefine/; +local *RT::Action::SendEmail::SendMessage = sub { return 1}; + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); +$tick= $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); + +like (first_txn($tick)->Content , qr/FYI/, "We recorded the content right"); +is (count_attachs($tick) , 5 , "Has three attachments"); + + + + +# }}} + +# {{{ test a multi-line RT-Send-CC header + + my $rt_send_cc_email = RT::Test::get_relocatable_file('rt-send-cc', + (File::Spec->updir(), 'data', 'emails')); + $content = RT::Test->file_content($rt_send_cc_email); + +$parser->ParseMIMEEntityFromScalar($content); + + + + %args = (message => $content, queue => 1, action => 'correspond'); + RT::Interface::Email::Gateway(\%args); + $tickets = RT::Tickets->new($RT::SystemUser); +$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC'); +$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0'); +$tick= $tickets->First(); +ok ($tick->Id, "found ticket ".$tick->Id); + +my $cc = first_attach($tick)->GetHeader('RT-Send-Cc'); +like ($cc , qr/test1/, "Found test 1"); +like ($cc , qr/test2/, "Found test 2"); +like ($cc , qr/test3/, "Found test 3"); +like ($cc , qr/test4/, "Found test 4"); +like ($cc , qr/test5/, "Found test 5"); + +# }}} + +diag q{regression test for #5248 from rt3.fsck.com} if $ENV{TEST_VERBOSE}; +{ + my $subject_folding_email = RT::Test::get_relocatable_file( + 'subject-with-folding-ws', (File::Spec->updir(), 'data', 'emails')); + my $content = RT::Test->file_content($subject_folding_email); + my ($status, $msg, $ticket) = RT::Interface::Email::Gateway( + { message => $content, queue => 1, action => 'correspond' } + ); + ok ($status, 'created ticket') or diag "error: $msg"; + ok ($ticket->id, "found ticket ". $ticket->id); + is ($ticket->Subject, 'test', 'correct subject'); +} + +diag q{regression test for #5248 from rt3.fsck.com} if $ENV{TEST_VERBOSE}; +{ + my $long_subject_email = RT::Test::get_relocatable_file('very-long-subject', + (File::Spec->updir(), 'data', 'emails')); + my $content = RT::Test->file_content($long_subject_email); + my ($status, $msg, $ticket) = RT::Interface::Email::Gateway( + { message => $content, queue => 1, action => 'correspond' } + ); + ok ($status, 'created ticket') or diag "error: $msg"; + ok ($ticket->id, "found ticket ". $ticket->id); + is ($ticket->Subject, '0123456789'x20, 'correct subject'); +} + + + +# Don't taint the environment +$everyone->PrincipalObj->RevokeRight(Right =>'SuperUser'); +1; diff --git a/rt/t/mail/verp.t b/rt/t/mail/verp.t new file mode 100644 index 000000000..79ede90ab --- /dev/null +++ b/rt/t/mail/verp.t @@ -0,0 +1,8 @@ +#!/usr/bin/perl -w + +use strict; +use RT::Test tests => 1; +TODO: { + todo_skip "No tests written for VERP yet", 1; + ok(1,"a test to skip"); +} diff --git a/rt/t/maildigest/attributes.t b/rt/t/maildigest/attributes.t new file mode 100644 index 000000000..ba2a58566 --- /dev/null +++ b/rt/t/maildigest/attributes.t @@ -0,0 +1,168 @@ +#!/usr/bin/perl -w + +use warnings; +use strict; +use RT; +use RT::Test tests => 31; +my @users = qw/ emailnormal@example.com emaildaily@example.com emailweekly@example.com emailsusp@example.com /; + +my( $ret, $msg ); +my $user_n = RT::User->new( $RT::SystemUser ); +( $ret, $msg ) = $user_n->LoadOrCreateByEmail( $users[0] ); +ok( $ret, "user with default email prefs created: $msg" ); +$user_n->SetPrivileged( 1 ); + +my $user_d = RT::User->new( $RT::SystemUser ); +( $ret, $msg ) = $user_d->LoadOrCreateByEmail( $users[1] ); +ok( $ret, "user with daily digest email prefs created: $msg" ); +# Set a username & password for testing the interface. +$user_d->SetPrivileged( 1 ); +$user_d->SetPreferences($RT::System => { %{ $user_d->Preferences( $RT::System ) || {}}, EmailFrequency => 'Daily digest'}); + + + +my $user_w = RT::User->new( $RT::SystemUser ); +( $ret, $msg ) = $user_w->LoadOrCreateByEmail( $users[2] ); +ok( $ret, "user with weekly digest email prefs created: $msg" ); +$user_w->SetPrivileged( 1 ); +$user_w->SetPreferences($RT::System => { %{ $user_w->Preferences( $RT::System ) || {}}, EmailFrequency => 'Weekly digest'}); + +my $user_s = RT::User->new( $RT::SystemUser ); +( $ret, $msg ) = $user_s->LoadOrCreateByEmail( $users[3] ); +ok( $ret, "user with suspended email prefs created: $msg" ); +$user_s->SetPreferences($RT::System => { %{ $user_s->Preferences( $RT::System ) || {}}, EmailFrequency => 'Suspended'}); +$user_s->SetPrivileged( 1 ); + + +is(RT::Config->Get('EmailFrequency' => $user_s), 'Suspended'); + +# Make a testing queue for ourselves. +my $testq = RT::Queue->new( $RT::SystemUser ); +if( $testq->ValidateName( 'EmailDigest-testqueue' ) ) { + ( $ret, $msg ) = $testq->Create( Name => 'EmailDigest-testqueue' ); + ok( $ret, "Our test queue is created: $msg" ); +} else { + $testq->Load( 'EmailDigest-testqueue' ); + ok( $testq->id, "Our test queue is loaded" ); +} + +# Allow anyone to open a ticket on the test queue. +my $everyone = RT::Group->new( $RT::SystemUser ); +( $ret, $msg ) = $everyone->LoadSystemInternalGroup( 'Everyone' ); +ok( $ret, "Loaded 'everyone' group: $msg" ); + +( $ret, $msg ) = $everyone->PrincipalObj->GrantRight( Right => 'CreateTicket', + Object => $testq ); +ok( $ret || $msg =~ /already has/, "Granted everyone CreateTicket on testq: $msg" ); + +# Make user_d an admincc for the queue. +( $ret, $msg ) = $user_d->PrincipalObj->GrantRight( Right => 'AdminQueue', + Object => $testq ); +ok( $ret || $msg =~ /already has/, "Granted dduser AdminQueue on testq: $msg" ); +( $ret, $msg ) = $testq->AddWatcher( Type => 'AdminCc', + PrincipalId => $user_d->PrincipalObj->id ); +ok( $ret || $msg =~ /already/, "dduser added as a queue watcher: $msg" ); + +# Give the others queue rights. +( $ret, $msg ) = $user_n->PrincipalObj->GrantRight( Right => 'AdminQueue', + Object => $testq ); +ok( $ret || $msg =~ /already has/, "Granted emailnormal right on testq: $msg" ); +( $ret, $msg ) = $user_w->PrincipalObj->GrantRight( Right => 'AdminQueue', + Object => $testq ); +ok( $ret || $msg =~ /already has/, "Granted emailweekly right on testq: $msg" ); +( $ret, $msg ) = $user_s->PrincipalObj->GrantRight( Right => 'AdminQueue', + Object => $testq ); +ok( $ret || $msg =~ /already has/, "Granted emailsusp right on testq: $msg" ); + +# Create a ticket with To: Cc: Bcc: fields using our four users. +my $id; +my $ticket = RT::Ticket->new( $RT::SystemUser ); +( $id, $ret, $msg ) = $ticket->Create( Queue => $testq->Name, + Requestor => [ $user_w->Name ], + Subject => 'Test ticket for RT::Extension::EmailDigest', + ); +ok( $ret, "Ticket $id created: $msg" ); + +# Make the other users ticket watchers. +( $ret, $msg ) = $ticket->AddWatcher( Type => 'Cc', + PrincipalId => $user_n->PrincipalObj->id ); +ok( $ret, "Added user_n as a ticket watcher: $msg" ); +( $ret, $msg ) = $ticket->AddWatcher( Type => 'Cc', + PrincipalId => $user_s->PrincipalObj->id ); +ok( $ret, "Added user_s as a ticket watcher: $msg" ); + +my $obj; +($id, $msg, $obj ) = $ticket->Correspond( + Content => "This is a ticket response for CC action" ); +ok( $ret, "Transaction created: $msg" ); + +# Get the deferred notifications that should result. Should be two for +# email daily, and one apiece for emailweekly and emailsusp. +my @notifications; + +my $txns = RT::Transactions->new( $RT::SystemUser ); +$txns->LimitToTicket( $ticket->id ); +my( $c_daily, $c_weekly, $c_susp ) = ( 0, 0, 0 ); +while( my $txn = $txns->Next ) { + my @daily_rcpt = $txn->DeferredRecipients( 'daily' ); + my @weekly_rcpt = $txn->DeferredRecipients('weekly' ); + my @susp_rcpt = $txn->DeferredRecipients( 'susp' ); + + $c_daily++ if @daily_rcpt; + $c_weekly++ if @weekly_rcpt; + $c_susp++ if @susp_rcpt; + + # If the transaction has content... + if( $txn->ContentObj ) { + # ...none of the deferred folk should be in the header. + my $headerstr = $txn->ContentObj->Headers; + foreach my $rcpt( @daily_rcpt, @weekly_rcpt, @susp_rcpt ) { + ok( $headerstr !~ /$rcpt/, "Deferred recipient $rcpt not found in header" ); + } + } +} + +# Finally, check to see that we got the correct number of each sort of +# deferred recipient. +is( $c_daily, 2, "correct number of daily-sent messages" ); +is( $c_weekly, 2, "correct number of weekly-sent messages" ); +is( $c_susp, 1, "correct number of suspended messages" ); + + + + + +# Now let's actually run the daily and weekly digest tool to make sure we generate those + +# the first time get the content +email_digest_like( '--mode daily --print', qr/in the last day/ ); +# The second time run it for real so we make sure that we get RT to mark the txn as sent +email_digest_like( '--mode daily', qr/maildaily\@/ ); +# now we should have nothing to do, so no content. +email_digest_like( '--mode daily --print', '' ); + +# the first time get the content +email_digest_like( '--mode weekly --print', qr/in the last seven days/ ); +# The second time run it for real so we make sure that we get RT to mark the txn as sent +email_digest_like( '--mode weekly', qr/mailweekly\@/ ); +# now we should have nothing to do, so no content. +email_digest_like( '--mode weekly --print', '' ); + +sub email_digest_like { + my $arg = shift; + my $pattern = shift; + + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my $perl = $^X . ' ' . join ' ', map { "-I$_" } @INC; + open my $digester, "-|", "$perl $RT::SbinPath/rt-email-digest $arg"; + my @results = <$digester>; + my $content = join '', @results; + if ( ref $pattern && ref $pattern eq 'Regexp' ) { + like($content, $pattern); + } + else { + is( $content, $pattern ); + } + close $digester; +} diff --git a/rt/t/pod.t b/rt/t/pod.t new file mode 100644 index 000000000..d11a497eb --- /dev/null +++ b/rt/t/pod.t @@ -0,0 +1,7 @@ +use strict; +use warnings; + +use Test::More; +eval "use Test::Pod 1.14"; +plan skip_all => "Test::Pod 1.14 required for testing POD" if $@; +all_pod_files_ok(); diff --git a/rt/t/rtname.t b/rt/t/rtname.t new file mode 100644 index 000000000..ef6092bb2 --- /dev/null +++ b/rt/t/rtname.t @@ -0,0 +1,34 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test nodata => 1, tests => 9; + +use RT::Interface::Email; + +# normal use case, regexp set to rtname +RT->Config->Set( rtname => "site" ); +RT->Config->Set( EmailSubjectTagRegex => qr/site/ ); +RT->Config->Set( rtname => undef ); +is(RT::Interface::Email::ParseTicketId("[site #123] test"), 123); +is(RT::Interface::Email::ParseTicketId("[othersite #123] test"), undef); + +# oops usecase, where the regexp is scragged +RT->Config->Set( rtname => "site" ); +RT->Config->Set( EmailSubjectTagRegex => undef ); +is(RT::Interface::Email::ParseTicketId("[site #123] test"), 123); +is(RT::Interface::Email::ParseTicketId("[othersite #123] test"), undef); + +# set to a simple regexp. NOTE: we no longer match "site" +RT->Config->Set( rtname => "site"); +RT->Config->Set( EmailSubjectTagRegex => qr/newsite/); +is(RT::Interface::Email::ParseTicketId("[site #123] test"), undef); +is(RT::Interface::Email::ParseTicketId("[newsite #123] test"), 123); + +# set to a more complex regexp +RT->Config->Set( rtname => "site" ); +RT->Config->Set( EmailSubjectTagRegex => qr/newsite|site/ ); +is(RT::Interface::Email::ParseTicketId("[site #123] test"), 123); +is(RT::Interface::Email::ParseTicketId("[newsite #123] test"), 123); +is(RT::Interface::Email::ParseTicketId("[othersite #123] test"), undef); + diff --git a/rt/t/savedsearch.t b/rt/t/savedsearch.t new file mode 100644 index 000000000..5798f79cb --- /dev/null +++ b/rt/t/savedsearch.t @@ -0,0 +1,185 @@ +use strict; +use warnings; +BEGIN { $ENV{'LANG'} = 'C' } +use RT; +use RT::User; +use RT::Group; +use RT::Ticket; +use RT::Queue; + +use RT::Test tests => 27; +use_ok('RT::SavedSearch'); +use_ok('RT::SavedSearches'); + +use Test::Warn; + +# Set up some infrastructure. These calls are tested elsewhere. + +my $searchuser = RT::User->new($RT::SystemUser); +my ($ret, $msg) = $searchuser->Create(Name => 'searchuser'.$$, + Privileged => 1, + EmailAddress => "searchuser\@p$$.example.com", + RealName => 'Search user'); +ok($ret, "created searchuser: $msg"); +$searchuser->PrincipalObj->GrantRight(Right => 'LoadSavedSearch'); +$searchuser->PrincipalObj->GrantRight(Right => 'CreateSavedSearch'); +$searchuser->PrincipalObj->GrantRight(Right => 'ModifySelf'); + +# This is the group whose searches searchuser should be able to see. +my $ingroup = RT::Group->new($RT::SystemUser); +$ingroup->CreateUserDefinedGroup(Name => 'searchgroup1'.$$); +$ingroup->AddMember($searchuser->Id); +$searchuser->PrincipalObj->GrantRight(Right => 'EditSavedSearches', + Object => $ingroup); +$searchuser->PrincipalObj->GrantRight(Right => 'ShowSavedSearches', + Object => $ingroup); + +# This is the group whose searches searchuser should not be able to see. +my $outgroup = RT::Group->new($RT::SystemUser); +$outgroup->CreateUserDefinedGroup(Name => 'searchgroup2'.$$); +$outgroup->AddMember($RT::SystemUser->Id); + +my $queue = RT::Queue->new($RT::SystemUser); +$queue->Create(Name => 'SearchQueue'.$$); +$searchuser->PrincipalObj->GrantRight(Right => 'SeeQueue', Object => $queue); +$searchuser->PrincipalObj->GrantRight(Right => 'ShowTicket', Object => $queue); +$searchuser->PrincipalObj->GrantRight(Right => 'OwnTicket', Object => $queue); + + +my $ticket = RT::Ticket->new($RT::SystemUser); +$ticket->Create(Queue => $queue->Id, + Requestor => [ $searchuser->Name ], + Owner => $searchuser, + Subject => 'saved search test'); + + +# Now start the search madness. +my $curruser = RT::CurrentUser->new($searchuser); +my $format = '\' <b><a href="/Ticket/Display.html?id=__id__">__id__</a></b>/TITLE:#\', +\'<b><a href="/Ticket/Display.html?id=__id__">__Subject__</a></b>/TITLE:Subject\', +\'__Status__\', +\'__QueueName__\', +\'__OwnerName__\', +\'__Priority__\', +\'__NEWLINE__\', +\'\', +\'<small>__Requestors__</small>\', +\'<small>__CreatedRelative__</small>\', +\'<small>__ToldRelative__</small>\', +\'<small>__LastUpdatedRelative__</small>\', +\'<small>__TimeLeft__</small>\''; + +my $mysearch = RT::SavedSearch->new($curruser); +($ret, $msg) = $mysearch->Save(Privacy => 'RT::User-' . $searchuser->Id, + Type => 'Ticket', + Name => 'owned by me', + SearchParams => {'Format' => $format, + 'Query' => "Owner = '" + . $searchuser->Name + . "'"}); +ok($ret, "mysearch was created"); + + +my $groupsearch = RT::SavedSearch->new($curruser); +($ret, $msg) = $groupsearch->Save(Privacy => 'RT::Group-' . $ingroup->Id, + Type => 'Ticket', + Name => 'search queue', + SearchParams => {'Format' => $format, + 'Query' => "Queue = '" + . $queue->Name . "'"}); +ok($ret, "groupsearch was created"); + +my $othersearch = RT::SavedSearch->new($curruser); +($ret, $msg) = $othersearch->Save(Privacy => 'RT::Group-' . $outgroup->Id, + Type => 'Ticket', + Name => 'searchuser requested', + SearchParams => {'Format' => $format, + 'Query' => + "Requestor.Name LIKE 'search'"}); +ok(!$ret, "othersearch NOT created"); +like($msg, qr/Failed to load object for/, "...for the right reason"); + +$othersearch = RT::SavedSearch->new($RT::SystemUser); +($ret, $msg) = $othersearch->Save(Privacy => 'RT::Group-' . $outgroup->Id, + Type => 'Ticket', + Name => 'searchuser requested', + SearchParams => {'Format' => $format, + 'Query' => + "Requestor.Name LIKE 'search'"}); +ok($ret, "othersearch created by systemuser"); + +# Now try to load some searches. + +# This should work. +my $loadedsearch1 = RT::SavedSearch->new($curruser); +$loadedsearch1->Load('RT::User-'.$curruser->Id, $mysearch->Id); +is($loadedsearch1->Id, $mysearch->Id, "Loaded mysearch"); +like($loadedsearch1->GetParameter('Query'), qr/Owner/, + "Retrieved query of mysearch"); +# Check through the other accessor methods. +is($loadedsearch1->Privacy, 'RT::User-' . $curruser->Id, + "Privacy of mysearch correct"); +is($loadedsearch1->Name, 'owned by me', "Name of mysearch correct"); +is($loadedsearch1->Type, 'Ticket', "Type of mysearch correct"); + +# See if it can be used to search for tickets. +my $tickets = RT::Tickets->new($curruser); +$tickets->FromSQL($loadedsearch1->GetParameter('Query')); +is($tickets->Count, 1, "Found a ticket"); + +# This should fail -- wrong object. +# my $loadedsearch2 = RT::SavedSearch->new($curruser); +# $loadedsearch2->Load('RT::User-'.$curruser->Id, $groupsearch->Id); +# isnt($loadedsearch2->Id, $othersearch->Id, "Didn't load groupsearch as mine"); +# ...but this should succeed. +my $loadedsearch3 = RT::SavedSearch->new($curruser); +$loadedsearch3->Load('RT::Group-'.$ingroup->Id, $groupsearch->Id); +is($loadedsearch3->Id, $groupsearch->Id, "Loaded groupsearch"); +like($loadedsearch3->GetParameter('Query'), qr/Queue/, + "Retrieved query of groupsearch"); +# Can it get tickets? +$tickets = RT::Tickets->new($curruser); +$tickets->FromSQL($loadedsearch3->GetParameter('Query')); +is($tickets->Count, 1, "Found a ticket"); + +# This should fail -- no permission. +my $loadedsearch4 = RT::SavedSearch->new($curruser); + +warning_like { + $loadedsearch4->Load($othersearch->Privacy, $othersearch->Id); +} qr/Could not load object RT::Group-\d+ when loading search/; + +isnt($loadedsearch4->Id, $othersearch->Id, "Did not load othersearch"); + +# Try to update an existing search. +$loadedsearch1->Update( SearchParams => {'Format' => $format, + 'Query' => "Queue = '" . $queue->Name . "'" } ); +like($loadedsearch1->GetParameter('Query'), qr/Queue/, + "Updated mysearch parameter"); +is($loadedsearch1->Type, 'Ticket', "mysearch is still for tickets"); +is($loadedsearch1->Privacy, 'RT::User-'.$curruser->Id, + "mysearch still belongs to searchuser"); +like($mysearch->GetParameter('Query'), qr/Queue/, "other mysearch object updated"); + + +## Right ho. Test the pseudo-collection object. + +my $genericsearch = RT::SavedSearch->new($curruser); +$genericsearch->Save(Name => 'generic search', + Type => 'all', + SearchParams => {'Query' => "Queue = 'General'"}); + +my $ticketsearches = RT::SavedSearches->new($curruser); +$ticketsearches->LimitToPrivacy('RT::User-'.$curruser->Id, 'Ticket'); +is($ticketsearches->Count, 1, "Found searchuser's ticket searches"); + +my $allsearches = RT::SavedSearches->new($curruser); +$allsearches->LimitToPrivacy('RT::User-'.$curruser->Id); +is($allsearches->Count, 2, "Found all searchuser's searches"); + +# Delete a search. +($ret, $msg) = $genericsearch->Delete; +ok($ret, "Deleted genericsearch"); +$allsearches->LimitToPrivacy('RT::User-'.$curruser->Id); +is($allsearches->Count, 1, "Found all searchuser's searches after deletion"); + diff --git a/rt/t/shredder/00load.t b/rt/t/shredder/00load.t new file mode 100644 index 000000000..1e06261bc --- /dev/null +++ b/rt/t/shredder/00load.t @@ -0,0 +1,29 @@ +use strict; +use warnings; +use File::Spec; +use Test::More tests => 11; +use RT::Test (); + +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} + +use_ok("RT::Shredder"); + +use_ok("RT::Shredder::Plugin"); +use_ok("RT::Shredder::Plugin::Base"); + +# search plugins +use_ok("RT::Shredder::Plugin::Base::Search"); +use_ok("RT::Shredder::Plugin::Objects"); +use_ok("RT::Shredder::Plugin::Attachments"); +use_ok("RT::Shredder::Plugin::Tickets"); +use_ok("RT::Shredder::Plugin::Users"); + +# dump plugins +use_ok("RT::Shredder::Plugin::Base::Dump"); +use_ok("RT::Shredder::Plugin::SQLDump"); +use_ok("RT::Shredder::Plugin::Summary"); + diff --git a/rt/t/shredder/00skeleton.t b/rt/t/shredder/00skeleton.t new file mode 100644 index 000000000..eab9433cd --- /dev/null +++ b/rt/t/shredder/00skeleton.t @@ -0,0 +1,25 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 1; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} +init_db(); + + +create_savepoint('clean'); # backup of the clean RT DB +my $shredder = shredder_new(); # new shredder object + +# .... +# create and wipe RT objects +# + +cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); diff --git a/rt/t/shredder/01basics.t b/rt/t/shredder/01basics.t new file mode 100644 index 000000000..450f2df8c --- /dev/null +++ b/rt/t/shredder/01basics.t @@ -0,0 +1,32 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 3; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} +init_db(); + + +create_savepoint(); + +use RT::Tickets; +my $ticket = RT::Ticket->new( $RT::SystemUser ); +my ($id) = $ticket->Create( Subject => 'test', Queue => 1 ); +ok( $id, "created new ticket" ); + +$ticket = RT::Ticket->new( $RT::SystemUser ); +my ($status, $msg) = $ticket->Load( $id ); +ok( $id, "load ticket" ) or diag( "error: $msg" ); + +my $shredder = shredder_new(); +$shredder->Wipeout( Object => $ticket ); + +cmp_deeply( dump_current_and_savepoint(), "current DB equal to savepoint"); diff --git a/rt/t/shredder/01ticket.t b/rt/t/shredder/01ticket.t new file mode 100644 index 000000000..5625b985d --- /dev/null +++ b/rt/t/shredder/01ticket.t @@ -0,0 +1,86 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 15; +use RT::Test (); + + +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} + +init_db(); +create_savepoint('clean'); + +use RT::Ticket; +use RT::Tickets; + +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + my ($id) = $ticket->Create( Subject => 'test', Queue => 1 ); + ok( $id, "created new ticket" ); + $ticket->Delete; + is( $ticket->Status, 'deleted', "successfuly changed status" ); + + my $tickets = RT::Tickets->new( $RT::SystemUser ); + $tickets->{'allow_deleted_search'} = 1; + $tickets->LimitStatus( VALUE => 'deleted' ); + is( $tickets->Count, 1, "found one deleted ticket" ); + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $tickets ); + $shredder->WipeoutAll; +} +cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); + +{ + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($pid) = $parent->Create( Subject => 'test', Queue => 1 ); + ok( $pid, "created new ticket" ); + create_savepoint('parent_ticket'); + + my $child = RT::Ticket->new( $RT::SystemUser ); + my ($cid) = $child->Create( Subject => 'test', Queue => 1 ); + ok( $cid, "created new ticket" ); + + my ($status, $msg) = $parent->AddLink( Type => 'MemberOf', Target => $cid ); + ok( $status, "Added link between tickets") or diag("error: $msg"); + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $child ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint"); + + $shredder->PutObjects( Objects => $parent ); + $shredder->WipeoutAll; +} +cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); + +{ + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($pid) = $parent->Create( Subject => 'test', Queue => 1 ); + ok( $pid, "created new ticket" ); + my ($status, $msg) = $parent->Delete; + ok( $status, 'deleted parent ticket'); + create_savepoint('parent_ticket'); + + my $child = RT::Ticket->new( $RT::SystemUser ); + my ($cid) = $child->Create( Subject => 'test', Queue => 1 ); + ok( $cid, "created new ticket" ); + + ($status, $msg) = $parent->AddLink( Type => 'DependsOn', Target => $cid ); + ok( $status, "Added link between tickets") or diag("error: $msg"); + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $child ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint"); + + $shredder->PutObjects( Objects => $parent ); + $shredder->WipeoutAll; +} +cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); diff --git a/rt/t/shredder/02group_member.t b/rt/t/shredder/02group_member.t new file mode 100644 index 000000000..b68557a8b --- /dev/null +++ b/rt/t/shredder/02group_member.t @@ -0,0 +1,103 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 22; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} +init_db(); + + +### nested membership check +{ + create_savepoint('clean'); + my $pgroup = RT::Group->new( $RT::SystemUser ); + my ($pgid) = $pgroup->CreateUserDefinedGroup( Name => 'Parent group' ); + ok( $pgid, "created parent group" ); + is( $pgroup->id, $pgid, "id is correct" ); + + my $cgroup = RT::Group->new( $RT::SystemUser ); + my ($cgid) = $cgroup->CreateUserDefinedGroup( Name => 'Child group' ); + ok( $cgid, "created child group" ); + is( $cgroup->id, $cgid, "id is correct" ); + + my ($status, $msg) = $pgroup->AddMember( $cgroup->id ); + ok( $status, "added child group to parent") or diag "error: $msg"; + + create_savepoint('bucreate'); # before user create + my $user = RT::User->new( $RT::SystemUser ); + my $uid; + ($uid, $msg) = $user->Create( Name => 'new user', Privileged => 1, Disabled => 0 ); + ok( $uid, "created new user" ) or diag "error: $msg"; + is( $user->id, $uid, "id is correct" ); + + create_savepoint('buadd'); # before group add + ($status, $msg) = $cgroup->AddMember( $user->id ); + ok( $status, "added user to child group") or diag "error: $msg"; + + my $members = RT::GroupMembers->new( $RT::SystemUser ); + $members->Limit( FIELD => 'MemberId', VALUE => $uid ); + $members->Limit( FIELD => 'GroupId', VALUE => $cgid ); + is( $members->Count, 1, "find membership record" ); + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $members ); + $shredder->WipeoutAll(); + cmp_deeply( dump_current_and_savepoint('buadd'), "current DB equal to savepoint"); + + $shredder->PutObjects( Objects => $user ); + $shredder->WipeoutAll(); + cmp_deeply( dump_current_and_savepoint('bucreate'), "current DB equal to savepoint"); + + $shredder->PutObjects( Objects => [$pgroup, $cgroup] ); + $shredder->WipeoutAll(); + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} + +### deleting member of the ticket Owner role group +{ + restore_savepoint('clean'); + + my $user = RT::User->new( $RT::SystemUser ); + my ($uid, $msg) = $user->Create( Name => 'new user', Privileged => 1, Disabled => 0 ); + ok( $uid, "created new user" ) or diag "error: $msg"; + is( $user->id, $uid, "id is correct" ); + + use RT::Queue; + my $queue = new RT::Queue( $RT::SystemUser ); + $queue->Load('general'); + ok( $queue->id, "queue loaded succesfully" ); + + $user->PrincipalObj->GrantRight( Right => 'OwnTicket', Object => $queue ); + + use RT::Tickets; + my $ticket = RT::Ticket->new( $RT::SystemUser ); + my ($id) = $ticket->Create( Subject => 'test', Queue => $queue->id ); + ok( $id, "created new ticket" ); + $ticket = RT::Ticket->new( $RT::SystemUser ); + my $status; + ($status, $msg) = $ticket->Load( $id ); + ok( $id, "load ticket" ) or diag( "error: $msg" ); + + ($status, $msg) = $ticket->SetOwner( $user->id ); + ok( $status, "owner successfuly set") or diag( "error: $msg" ); + is( $ticket->Owner, $user->id, "owner successfuly set") or diag( "error: $msg" ); + + my $member = $ticket->OwnerGroup->MembersObj->First; + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $member ); + $shredder->WipeoutAll(); + + $ticket = RT::Ticket->new( $RT::SystemUser ); + ($status, $msg) = $ticket->Load( $id ); + ok( $id, "load ticket" ) or diag( "error: $msg" ); + is( $ticket->Owner, $RT::Nobody->id, "owner switched back to nobody" ); + is( $ticket->OwnerGroup->MembersObj->First->MemberId, $RT::Nobody->id, "and owner role group member is nobody"); +} diff --git a/rt/t/shredder/02queue.t b/rt/t/shredder/02queue.t new file mode 100644 index 000000000..197cf63c8 --- /dev/null +++ b/rt/t/shredder/02queue.t @@ -0,0 +1,125 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 16; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} +init_db(); + + +diag 'simple queue' if $ENV{'TEST_VERBOSE'}; +{ + create_savepoint('clean'); + my $queue = RT::Queue->new( $RT::SystemUser ); + my ($id, $msg) = $queue->Create( Name => 'my queue' ); + ok($id, 'created queue') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $queue ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} + +diag 'queue with scrip' if $ENV{'TEST_VERBOSE'}; +{ + create_savepoint('clean'); + my $queue = RT::Queue->new( $RT::SystemUser ); + my ($id, $msg) = $queue->Create( Name => 'my queue' ); + ok($id, 'created queue') or diag "error: $msg"; + + my $scrip = RT::Scrip->new( $RT::SystemUser ); + ($id, $msg) = $scrip->Create( + Description => 'my scrip', + Queue => $queue->id, + ScripCondition => 'On Create', + ScripAction => 'Open Tickets', + Template => 'Blank', + ); + ok($id, 'created scrip') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $queue ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} + +diag 'queue with template' if $ENV{'TEST_VERBOSE'}; +{ + create_savepoint('clean'); + my $queue = RT::Queue->new( $RT::SystemUser ); + my ($id, $msg) = $queue->Create( Name => 'my queue' ); + ok($id, 'created queue') or diag "error: $msg"; + + my $template = RT::Template->new( $RT::SystemUser ); + ($id, $msg) = $template->Create( + Name => 'my template', + Queue => $queue->id, + Content => "\nsome content", + ); + ok($id, 'created template') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $queue ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} + +diag 'queue with a right granted' if $ENV{'TEST_VERBOSE'}; +{ + create_savepoint('clean'); + my $queue = RT::Queue->new( $RT::SystemUser ); + my ($id, $msg) = $queue->Create( Name => 'my queue' ); + ok($id, 'created queue') or diag "error: $msg"; + + my $group = RT::Group->new( $RT::SystemUser ); + $group->LoadSystemInternalGroup('Everyone'); + ok($group->id, 'loaded group'); + + ($id, $msg) = $group->PrincipalObj->GrantRight( + Right => 'CreateTicket', + Object => $queue, + ); + ok($id, 'granted right') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $queue ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} + +diag 'queue with a watcher' if $ENV{'TEST_VERBOSE'}; +{ +# XXX, FIXME: if uncomment these lines then we'll get 'Bizarre...' +# create_savepoint('clean'); + my $group = RT::Group->new( $RT::SystemUser ); + my ($id, $msg) = $group->CreateUserDefinedGroup(Name => 'my group'); + ok($id, 'created group') or diag "error: $msg"; + + create_savepoint('bqcreate'); + my $queue = RT::Queue->new( $RT::SystemUser ); + ($id, $msg) = $queue->Create( Name => 'my queue' ); + ok($id, 'created queue') or diag "error: $msg"; + + ($id, $msg) = $queue->AddWatcher( + Type => 'Cc', + PrincipalId => $group->id, + ); + ok($id, 'added watcher') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $queue ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('bqcreate'), "current DB equal to savepoint"); + +# $shredder->PutObjects( Objects => $group ); +# $shredder->WipeoutAll; +# cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} diff --git a/rt/t/shredder/02template.t b/rt/t/shredder/02template.t new file mode 100644 index 000000000..d4c323e09 --- /dev/null +++ b/rt/t/shredder/02template.t @@ -0,0 +1,76 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 7; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} +init_db(); + + +diag 'global template' if $ENV{'TEST_VERBOSE'}; +{ + create_savepoint('clean'); + my $template = RT::Template->new( $RT::SystemUser ); + my ($id, $msg) = $template->Create( + Name => 'my template', + Content => "\nsome content", + ); + ok($id, 'created template') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $template ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} + +diag 'local template' if $ENV{'TEST_VERBOSE'}; +{ + create_savepoint('clean'); + my $template = RT::Template->new( $RT::SystemUser ); + my ($id, $msg) = $template->Create( + Name => 'my template', + Queue => 'General', + Content => "\nsome content", + ); + ok($id, 'created template') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $template ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} + +diag 'template used in scrip' if $ENV{'TEST_VERBOSE'}; +{ + create_savepoint('clean'); + my $template = RT::Template->new( $RT::SystemUser ); + my ($id, $msg) = $template->Create( + Name => 'my template', + Queue => 'General', + Content => "\nsome content", + ); + ok($id, 'created template') or diag "error: $msg"; + + my $scrip = RT::Scrip->new( $RT::SystemUser ); + ($id, $msg) = $scrip->Create( + Description => 'my scrip', + Queue => 'General', + ScripCondition => 'On Create', + ScripAction => 'Open Tickets', + Template => $template->id, + ); + ok($id, 'created scrip') or diag "error: $msg"; + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => $template ); + $shredder->WipeoutAll; + cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); +} diff --git a/rt/t/shredder/02user.t b/rt/t/shredder/02user.t new file mode 100644 index 000000000..03abd6c69 --- /dev/null +++ b/rt/t/shredder/02user.t @@ -0,0 +1,62 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 8; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} +init_db(); + + +create_savepoint('clean'); + +my $queue = RT::Queue->new( $RT::SystemUser ); +my ($qid) = $queue->Load( 'General' ); +ok( $qid, "loaded queue" ); + +my $ticket = RT::Ticket->new( $RT::SystemUser ); +my ($tid) = $ticket->Create( Queue => $qid, Subject => 'test' ); +ok( $tid, "ticket created" ); + +create_savepoint('bucreate'); # berfore user create +my $user = RT::User->new( $RT::SystemUser ); +my ($uid, $msg) = $user->Create( Name => 'new user', Privileged => 1, Disabled => 0 ); +ok( $uid, "created new user" ) or diag "error: $msg"; +is( $user->id, $uid, "id is correct" ); +# HACK: set ticket props to enable VARIABLE dependencies +$ticket->__Set( Field => 'LastUpdatedBy', Value => $uid ); +create_savepoint('aucreate'); # after user create + +{ + my $resolver = sub { + my %args = (@_); + my $t = $args{'TargetObject'}; + my $resolver_uid = $RT::SystemUser->id; + foreach my $method ( qw(Creator LastUpdatedBy) ) { + next unless $t->_Accessible( $method => 'read' ); + $t->__Set( Field => $method, Value => $resolver_uid ); + } + }; + my $shredder = shredder_new(); + $shredder->PutResolver( BaseClass => 'RT::User', Code => $resolver ); + $shredder->Wipeout( Object => $user ); + cmp_deeply( dump_current_and_savepoint('bucreate'), "current DB equal to savepoint"); +} + +{ + restore_savepoint('aucreate'); + my $user = RT::User->new( $RT::SystemUser ); + $user->Load($uid); + ok($user->id, "loaded user after restore"); + my $shredder = shredder_new(); + eval { $shredder->Wipeout( Object => $user ) }; + ok($@, "wipeout throw exception if no resolvers"); + cmp_deeply( dump_current_and_savepoint('aucreate'), "current DB equal to savepoint"); +} diff --git a/rt/t/shredder/03plugin.t b/rt/t/shredder/03plugin.t new file mode 100644 index 000000000..190f40acf --- /dev/null +++ b/rt/t/shredder/03plugin.t @@ -0,0 +1,46 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 28; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} + +my @PLUGINS = sort qw(Attachments Base Objects SQLDump Summary Tickets Users); + +use_ok('RT::Shredder::Plugin'); +{ + my $plugin = new RT::Shredder::Plugin; + isa_ok($plugin, 'RT::Shredder::Plugin'); + my %plugins = $plugin->List; + cmp_deeply( [sort keys %plugins], [@PLUGINS], "correct plugins" ); +} +{ # test ->List as class method + my %plugins = RT::Shredder::Plugin->List; + cmp_deeply( [sort keys %plugins], [@PLUGINS], "correct plugins" ); +} +{ # reblessing on LoadByName + foreach (@PLUGINS) { + my $plugin = new RT::Shredder::Plugin; + isa_ok($plugin, 'RT::Shredder::Plugin'); + my ($status, $msg) = $plugin->LoadByName( $_ ); + ok($status, "loaded plugin by name") or diag("error: $msg"); + isa_ok($plugin, "RT::Shredder::Plugin::$_" ); + } +} +{ # error checking in LoadByName + my $plugin = new RT::Shredder::Plugin; + isa_ok($plugin, 'RT::Shredder::Plugin'); + my ($status, $msg) = $plugin->LoadByName; + ok(!$status, "not loaded plugin - empty name"); + ($status, $msg) = $plugin->LoadByName('Foo'); + ok(!$status, "not loaded plugin - not exist"); +} + diff --git a/rt/t/shredder/03plugin_summary.t b/rt/t/shredder/03plugin_summary.t new file mode 100644 index 000000000..30606af41 --- /dev/null +++ b/rt/t/shredder/03plugin_summary.t @@ -0,0 +1,23 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 4; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} + + +use_ok('RT::Shredder::Plugin'); +my $plugin_obj = new RT::Shredder::Plugin; +isa_ok($plugin_obj, 'RT::Shredder::Plugin'); +my ($status, $msg) = $plugin_obj->LoadByName('Summary'); +ok($status, 'loaded summary plugin') or diag "error: $msg"; +isa_ok($plugin_obj, 'RT::Shredder::Plugin::Summary'); + diff --git a/rt/t/shredder/03plugin_tickets.t b/rt/t/shredder/03plugin_tickets.t new file mode 100644 index 000000000..3d742ff83 --- /dev/null +++ b/rt/t/shredder/03plugin_tickets.t @@ -0,0 +1,150 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 44; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} + + +use_ok('RT::Shredder::Plugin::Tickets'); +{ + my $plugin = new RT::Shredder::Plugin::Tickets; + isa_ok($plugin, 'RT::Shredder::Plugin::Tickets'); + + is(lc $plugin->Type, 'search', 'correct type'); +} + +init_db(); +create_savepoint('clean'); +use_ok('RT::Ticket'); +use_ok('RT::Tickets'); + +{ # create parent and child and check functionality of 'with_linked' arg + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($pid) = $parent->Create( Subject => 'parent', Queue => 1 ); + ok( $pid, "created new ticket" ); + + my $child = RT::Ticket->new( $RT::SystemUser ); + my ($cid) = $child->Create( Subject => 'child', Queue => 1, MemberOf => $pid ); + ok( $cid, "created new ticket" ); + + my $plugin = new RT::Shredder::Plugin::Tickets; + isa_ok($plugin, 'RT::Shredder::Plugin::Tickets'); + + my ($status, $msg, @objs); + ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"' ); + ok($status, "plugin arguments are ok") or diag "error: $msg"; + + ($status, @objs) = $plugin->Run; + ok($status, "executed plugin successfully") or diag "error: @objs"; + @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs ); + is(scalar @objs, 1, "only one object in result set"); + is($objs[0]->id, $pid, "parent is in result set"); + + ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"', with_linked => 1 ); + ok($status, "plugin arguments are ok") or diag "error: $msg"; + + ($status, @objs) = $plugin->Run; + ok($status, "executed plugin successfully") or diag "error: @objs"; + @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs ); + my %has = map { $_->id => 1 } @objs; + is(scalar @objs, 2, "two objects in the result set"); + ok($has{$pid}, "parent is in the result set"); + ok($has{$cid}, "child is in the result set"); + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => \@objs ); + $shredder->WipeoutAll; +} +cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); + +{ # create parent and child and link them reqursively to check that we don't hang + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($pid) = $parent->Create( Subject => 'parent', Queue => 1 ); + ok( $pid, "created new ticket" ); + + my $child = RT::Ticket->new( $RT::SystemUser ); + my ($cid) = $child->Create( Subject => 'child', Queue => 1, MemberOf => $pid ); + ok( $cid, "created new ticket" ); + + my ($status, $msg) = $child->AddLink( Target => $pid, Type => 'DependsOn' ); + ok($status, "added reqursive link") or diag "error: $msg"; + + my $plugin = new RT::Shredder::Plugin::Tickets; + isa_ok($plugin, 'RT::Shredder::Plugin::Tickets'); + + my (@objs); + ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"' ); + ok($status, "plugin arguments are ok") or diag "error: $msg"; + + ($status, @objs) = $plugin->Run; + ok($status, "executed plugin successfully") or diag "error: @objs"; + @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs ); + is(scalar @objs, 1, "only one object in result set"); + is($objs[0]->id, $pid, "parent is in result set"); + + ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"', with_linked => 1 ); + ok($status, "plugin arguments are ok") or diag "error: $msg"; + + ($status, @objs) = $plugin->Run; + ok($status, "executed plugin successfully") or diag "error: @objs"; + @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs ); + is(scalar @objs, 2, "two objects in the result set"); + my %has = map { $_->id => 1 } @objs; + ok($has{$pid}, "parent is in the result set"); + ok($has{$cid}, "child is in the result set"); + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => \@objs ); + $shredder->WipeoutAll; +} +cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); + +{ # create parent and child and check functionality of 'apply_query_to_linked' arg + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($pid) = $parent->Create( Subject => 'parent', Queue => 1, Status => 'resolved' ); + ok( $pid, "created new ticket" ); + + my $child1 = RT::Ticket->new( $RT::SystemUser ); + my ($cid1) = $child1->Create( Subject => 'child', Queue => 1, MemberOf => $pid ); + ok( $cid1, "created new ticket" ); + my $child2 = RT::Ticket->new( $RT::SystemUser ); + my ($cid2) = $child2->Create( Subject => 'child', Queue => 1, MemberOf => $pid, Status => 'resolved' ); + ok( $cid2, "created new ticket" ); + + my $plugin = new RT::Shredder::Plugin::Tickets; + isa_ok($plugin, 'RT::Shredder::Plugin::Tickets'); + + my ($status, $msg) = $plugin->TestArgs( query => 'Status = "resolved"', apply_query_to_linked => 1 ); + ok($status, "plugin arguments are ok") or diag "error: $msg"; + + my @objs; + ($status, @objs) = $plugin->Run; + ok($status, "executed plugin successfully") or diag "error: @objs"; + @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs ); + is(scalar @objs, 2, "two objects in the result set"); + my %has = map { $_->id => 1 } @objs; + ok($has{$pid}, "parent is in the result set"); + ok(!$has{$cid1}, "first child is in the result set"); + ok($has{$cid2}, "second child is in the result set"); + + my $shredder = shredder_new(); + $shredder->PutObjects( Objects => \@objs ); + $shredder->WipeoutAll; + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $cid1 ); + is($ticket->id, $cid1, 'loaded ticket'); + + $shredder->PutObjects( Objects => $ticket ); + $shredder->WipeoutAll; +} +cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint"); diff --git a/rt/t/shredder/03plugin_users.t b/rt/t/shredder/03plugin_users.t new file mode 100644 index 000000000..45fc8a27e --- /dev/null +++ b/rt/t/shredder/03plugin_users.t @@ -0,0 +1,40 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test::Deep; +use File::Spec; +use Test::More tests => 9; +use RT::Test (); +BEGIN { + my $shredder_utils = RT::Test::get_relocatable_file('utils.pl', + File::Spec->curdir()); + require $shredder_utils; +} + + +my @ARGS = sort qw(limit status name member_of email replace_relations no_tickets); + +use_ok('RT::Shredder::Plugin::Users'); +{ + my $plugin = new RT::Shredder::Plugin::Users; + isa_ok($plugin, 'RT::Shredder::Plugin::Users'); + + is(lc $plugin->Type, 'search', 'correct type'); + + my @args = sort $plugin->SupportArgs; + cmp_deeply(\@args, \@ARGS, "support all args"); + + + my ($status, $msg) = $plugin->TestArgs( name => 'r??t*' ); + ok($status, "arg name = 'r??t*'") or diag("error: $msg"); + + for (qw(any disabled enabled)) { + my ($status, $msg) = $plugin->TestArgs( status => $_ ); + ok($status, "arg status = '$_'") or diag("error: $msg"); + } + ($status, $msg) = $plugin->TestArgs( status => '!@#' ); + ok(!$status, "bad 'status' arg value"); +} + diff --git a/rt/t/shredder/utils.pl b/rt/t/shredder/utils.pl new file mode 100644 index 000000000..54243318e --- /dev/null +++ b/rt/t/shredder/utils.pl @@ -0,0 +1,435 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use File::Spec; +use File::Temp 0.19 (); +require File::Path; +require File::Copy; +require Cwd; + +BEGIN { +### after: push @INC, qw(@RT_LIB_PATH@); + push @INC, qw(/opt/rt3/local/lib /opt/rt3/lib); +} +use RT::Shredder; + +# where to keep temporary generated test data +my $tmpdir = ''; + +=head1 DESCRIPTION + +RT::Shredder test suite utilities + +=head1 TESTING + +Since RT:Shredder 0.01_03 we have a test suite. You +can run tests and see if everything works as expected +before you try shredder on your actual data. +Tests also help in the development process. + +The test suite uses SQLite databases to store data in individual files, +so you could sun tests on your production servers without risking +damage to your production data. + +You'll want to run the test suite almost every time you install or update +the shredder distribution, especialy if you have local customizations of +the DB schema and/or RT code. + +Tests are one thing you can write even if you don't know much perl, +but want to learn more about RT's internals. New tests are very welcome. + +=head2 WRITING TESTS + +The shredder distribution has several files to help write new tests. + + t/shredder/utils.pl - this file, utilities + t/00skeleton.t - skeleteton .t file for new tests + +All tests follow this algorithm: + + require "t/shredder/utils.pl"; # plug in utilities + init_db(); # create new tmp RT DB and init RT API + # create RT data you want to be always in the RT DB + # ... + create_savepoint('mysp'); # create DB savepoint + # create data you want delete with shredder + # ... + # run shredder on the objects you've created + # ... + # check that shredder deletes things you want + # this command will compare savepoint DB with current + cmp_deeply( dump_current_and_savepoint('mysp'), "current DB equal to savepoint"); + # then you can create another object and delete it, then check again + +Savepoints are named and you can create two or more savepoints. + +=head1 FUNCTIONS + +=head2 RT CONFIG + +=head3 rewrite_rtconfig + +Call this sub after C<RT::LoadConfig>. It changes the RT config +options necessary to switch to a local SQLite database. + +=cut + +sub rewrite_rtconfig +{ + # database + config_set( '$DatabaseType' , 'SQLite' ); + config_set( '$DatabaseHost' , 'localhost' ); + config_set( '$DatabaseRTHost' , 'localhost' ); + config_set( '$DatabasePort' , '' ); + config_set( '$DatabaseUser' , 'rt_user' ); + config_set( '$DatabasePassword' , 'rt_pass' ); + config_set( '$DatabaseRequireSSL' , undef ); + # database file name + config_set( '$DatabaseName' , db_name() ); + + # generic logging + config_set( '$LogToSyslog' , undef ); + config_set( '$LogToScreen' , 'error' ); + config_set( '$LogStackTraces' , 'crit' ); + # logging to standalone file + config_set( '$LogToFile' , 'debug' ); + my $fname = File::Spec->catfile(create_tmpdir(), test_name() .".log"); + config_set( '$LogToFileNamed' , $fname ); +} + +=head3 config_set + +This sub is a helper used by C<rewrite_rtconfig>. You shouldn't +need to use it elsewhere unless you need to change other RT +configuration variables. + +=cut + +sub config_set { + my $opt = shift; + $opt =~ s/^[\$\%\@]//; + RT->Config->Set($opt, @_) +} + +=head2 DATABASES + +=head3 init_db + +Creates a new RT DB with initial data in a new test tmp dir. +Also runs RT::Init() and RT::InitLogging(). + +This is all you need to call to setup a testing environment +in most situations. + +=cut + +sub init_db +{ + create_tmpdir(); + RT::LoadConfig(); + rewrite_rtconfig(); + RT::InitLogging(); + + _init_db(); + + RT::Init(); + $SIG{__WARN__} = sub { $RT::Logger->warning( @_ ); warn @_ }; + $SIG{__DIE__} = sub { $RT::Logger->crit( @_ ) unless $^S; die @_ }; +} + +use IPC::Open2; +sub _init_db +{ + + + foreach ( qw(Type Host Port Name User Password) ) { + $ENV{ "RT_DB_". uc $_ } = RT->Config->Get("Database$_"); + } + my $rt_setup_database = RT::Test::get_relocatable_file( + 'rt-setup-database', (File::Spec->updir(), File::Spec->updir(), 'sbin')); + my $cmd = "$^X $rt_setup_database --action init 2>&1"; + + my ($child_out, $child_in); + my $pid = open2($child_out, $child_in, $cmd); + close $child_in; + my $result = do { local $/; <$child_out> }; + return $result; +} + +=head3 db_name + +Returns the absolute file path to the current DB. +It is <$tmpdir . test_name() .'.db'>. + +See also the C<test_name> function. + +=cut + +sub db_name { return File::Spec->catfile(create_tmpdir(), test_name() .".db") } + +=head3 connect_sqlite + +Returns connected DBI DB handle. + +Takes path to sqlite db. + +=cut + +sub connect_sqlite +{ + return DBI->connect("dbi:SQLite:dbname=". shift, "", ""); +} + +=head2 SHREDDER + +=head3 shredder_new + +Creates and returns a new RT::Shredder object. + +=cut + +sub shredder_new +{ + my $obj = new RT::Shredder; + + my $file = File::Spec->catfile( create_tmpdir(), test_name() .'.XXXX.sql' ); + $obj->AddDumpPlugin( Arguments => { + file_name => $file, + from_storage => 0, + } ); + + return $obj; +} + + +=head2 TEST FILES + +=head3 test_name + +Returns name of the test file running now with file extension and +directory names stripped. + +For example, it returns '00load' for the test file 't/00load.t'. + +=cut + +sub test_name +{ + my $name = $0; + $name =~ s/^.*[\\\/]//; + $name =~ s/\..*$//; + return $name; +} + +=head2 TEMPORARY DIRECTORY + +=head3 tmpdir + +Returns the absolute path to a tmp dir used in tests. + +=cut + +sub tmpdir { + if (-d $tmpdir) { + return $tmpdir; + } else { + $tmpdir = File::Temp->newdir(TEMPLATE => 'shredderXXXXX', CLEANUP => 0); + return $tmpdir; + } +} + +=head2 create_tmpdir + +Creates a tmp dir if one doesn't exist already. Returns tmpdir path. + +=cut + +sub create_tmpdir { my $n = tmpdir(); File::Path::mkpath( [$n] ); return $n } + +=head3 cleanup_tmp + +Deletes all the tmp dir used in the tests. +See also the C<test_name> function. + +=cut + +sub cleanup_tmp +{ + my $dir = File::Spec->catdir(tmpdir(), test_name()); + return File::Path::rmtree( File::Spec->catdir( tmpdir(), test_name() )); +} + +=head2 SAVEPOINTS + +=head3 savepoint_name + +Returns the absolute path to the named savepoint DB file. +Takes one argument - savepoint name, by default C<sp>. + +=cut + +sub savepoint_name +{ + my $name = shift || 'sp'; + return File::Spec->catfile( create_tmpdir(), test_name() .".$name.db" ); +} + +=head3 create_savepoint + +Creates savepoint DB from the current DB. +Takes name of the savepoint as argument. + +=head3 restore_savepoint + +Restores current DB to savepoint state. +Takes name of the savepoint as argument. + +=cut + +sub create_savepoint { return __cp_db( db_name() => savepoint_name( shift ) ) } +sub restore_savepoint { return __cp_db( savepoint_name( shift ) => db_name() ) } +sub __cp_db +{ + my( $orig, $dest ) = @_; + $RT::Handle->dbh->disconnect; + # DIRTY HACK: undef Handles to force reconnect + $RT::Handle = undef; + %DBIx::SearchBuilder::DBIHandle = (); + $DBIx::SearchBuilder::PrevHandle = undef; + + File::Copy::copy( $orig, $dest ) or die "Couldn't copy '$orig' => '$dest': $!"; + RT::ConnectToDatabase(); + return; +} + + +=head2 DUMPS + +=head3 dump_sqlite + +Returns DB dump as a complex hash structure: + { + TableName => { + #id => { + lc_field => 'value', + } + } + } + +Takes named argument C<CleanDates>. If true, clean all date fields from +dump. True by default. + +=cut + +sub dump_sqlite +{ + my $dbh = shift; + my %args = ( CleanDates => 1, @_ ); + + my $old_fhkn = $dbh->{'FetchHashKeyName'}; + $dbh->{'FetchHashKeyName'} = 'NAME_lc'; + + my $sth = $dbh->table_info( '', '', '%', 'TABLE' ) || die $DBI::err; + my @tables = keys %{$sth->fetchall_hashref( 'table_name' )}; + + my $res = {}; + foreach my $t( @tables ) { + next if lc($t) eq 'sessions'; + $res->{$t} = $dbh->selectall_hashref("SELECT * FROM $t", 'id'); + clean_dates( $res->{$t} ) if $args{'CleanDates'}; + die $DBI::err if $DBI::err; + } + + $dbh->{'FetchHashKeyName'} = $old_fhkn; + return $res; +} + +=head3 dump_current_and_savepoint + +Returns dump of the current DB and of the named savepoint. +Takes one argument - savepoint name. + +=cut + +sub dump_current_and_savepoint +{ + my $orig = savepoint_name( shift ); + die "Couldn't find savepoint file" unless -f $orig && -r _; + my $odbh = connect_sqlite( $orig ); + return ( dump_sqlite( $RT::Handle->dbh, @_ ), dump_sqlite( $odbh, @_ ) ); +} + +=head3 dump_savepoint_and_current + +Returns the same data as C<dump_current_and_savepoint> function, +but in reversed order. + +=cut + +sub dump_savepoint_and_current { return reverse dump_current_and_savepoint(@_) } + +sub clean_dates +{ + my $h = shift; + my $date_re = qr/^\d\d\d\d\-\d\d\-\d\d\s*\d\d\:\d\d(\:\d\d)?$/i; + foreach my $id ( keys %{ $h } ) { + next unless $h->{ $id }; + foreach ( keys %{ $h->{ $id } } ) { + delete $h->{$id}{$_} if $h->{$id}{$_} && + $h->{$id}{$_} =~ /$date_re/; + } + } +} + +=head2 NOTES + +Function that returns debug notes. + +=head3 note_on_fail + +Returns a note about debug info that you can display if tests fail. + +=cut + +sub note_on_fail +{ + my $name = test_name(); + my $tmpdir = tmpdir(); + return <<END; +Some tests in '$0' file failed. +You can find debug info in '$tmpdir' dir. +There should be: + $name.log - RT debug log file + $name.db - latest RT DB used while testing + $name.*.db - savepoint databases +See also perldoc t/shredder/utils.pl for how to use this info. +END +} + +=head2 OTHER + +=head3 all_were_successful + +Returns true if all tests that have already run were successful. + +=cut + +sub all_were_successful +{ + use Test::Builder; + my $Test = Test::Builder->new; + return grep( !$_, $Test->summary )? 0: 1; +} + +END { + return unless -e tmpdir(); + if ( all_were_successful() ) { + cleanup_tmp(); + } else { + diag( note_on_fail() ); + } +} + +1; diff --git a/rt/t/ticket/action_linear_escalate.t b/rt/t/ticket/action_linear_escalate.t new file mode 100644 index 000000000..38cd47ded --- /dev/null +++ b/rt/t/ticket/action_linear_escalate.t @@ -0,0 +1,100 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT; +use RT::Test tests => 17; + +my ($id, $msg); +my $RecordTransaction; +my $UpdateLastUpdated; + + +use_ok('RT::Action::LinearEscalate'); + +my $q = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $q && $q->id, 'loaded or created queue'; + +# rt-cron-tool uses Gecos name to get rt user, so we'd better create one +my $gecos = RT::Test->load_or_create_user( + Name => 'gecos', + Password => 'password', + Gecos => ($^O eq 'MSWin32') ? Win32::LoginName() : (getpwuid($<))[0], +); +ok $gecos && $gecos->id, 'loaded or created gecos user'; + +# get rid of all right permissions +$gecos->PrincipalObj->GrantRight( Right => 'SuperUser' ); + + +my $user = RT::Test->load_or_create_user( + Name => 'user', Password => 'password', +); +ok $user && $user->id, 'loaded or created user'; + +$user->PrincipalObj->GrantRight( Right => 'SuperUser' ); +my $current_user = RT::CurrentUser->new($RT::SystemUser); +($id, $msg) = $current_user->Load($user->id); +ok( $id, "Got current user? $msg" ); + +#defaults +$RecordTransaction = 0; +$UpdateLastUpdated = 1; +my $ticket2 = create_ticket_as_ok($current_user); +escalate_ticket_ok($ticket2); +ok( $ticket2->LastUpdatedBy != $user->id, "Set LastUpdated" ); +ok( $ticket2->Transactions->Last->Type =~ /Create/i, "Did not record a transaction" ); + +$RecordTransaction = 1; +$UpdateLastUpdated = 1; +my $ticket1 = create_ticket_as_ok($current_user); +escalate_ticket_ok($ticket1); +ok( $ticket1->LastUpdatedBy != $user->id, "Set LastUpdated" ); +ok( $ticket1->Transactions->Last->Type !~ /Create/i, "Recorded a transaction" ); + +$RecordTransaction = 0; +$UpdateLastUpdated = 0; +my $ticket3 = create_ticket_as_ok($current_user); +escalate_ticket_ok($ticket3); +ok( $ticket3->LastUpdatedBy == $user->id, "Did not set LastUpdated" ); +ok( $ticket3->Transactions->Last->Type =~ /Create/i, "Did not record a transaction" ); + +1; + + +sub create_ticket_as_ok { + my $user = shift; + + my $created = RT::Date->new($RT::SystemUser); + $created->Unix(time() - ( 7 * 24 * 60**2 )); + my $due = RT::Date->new($RT::SystemUser); + $due->Unix(time() + ( 7 * 24 * 60**2 )); + + my $ticket = RT::Ticket->new($user); + ($id, $msg) = $ticket->Create( Queue => $q->id, + Subject => "Escalation test", + Priority => 0, + InitialPriority => 0, + FinalPriority => 50, + ); + ok($id, "Created ticket? ".$id); + $ticket->__Set( Field => 'Created', + Value => $created->ISO, + ); + $ticket->__Set( Field => 'Due', + Value => $due->ISO, + ); + + return $ticket; +} + +sub escalate_ticket_ok { + my $ticket = shift; + my $id = $ticket->id; + print "$RT::BinPath/rt-crontool --search RT::Search::FromSQL --search-arg \"id = @{[$id]}\" --action RT::Action::LinearEscalate --action-arg \"RecordTransaction:$RecordTransaction; UpdateLastUpdated:$UpdateLastUpdated\"\n"; + print STDERR `$RT::BinPath/rt-crontool --search RT::Search::FromSQL --search-arg "id = @{[$id]}" --action RT::Action::LinearEscalate --action-arg "RecordTransaction:$RecordTransaction; UpdateLastUpdated:$UpdateLastUpdated"`; + + $ticket->Load($id); # reload, because otherwise we get the cached value + ok( $ticket->Priority != 0, "Escalated ticket" ); +} diff --git a/rt/t/ticket/add-watchers.t b/rt/t/ticket/add-watchers.t new file mode 100644 index 000000000..ae993a936 --- /dev/null +++ b/rt/t/ticket/add-watchers.t @@ -0,0 +1,167 @@ +#!/usr/bin/perl -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC +# <jesse.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use RT::Test tests => 32; + +use strict; +use warnings; +no warnings 'once'; + +use RT::Queue; +use RT::User; +use RT::Group; +use RT::Ticket; +use RT::CurrentUser; + + +# clear all global right +my $acl = RT::ACL->new($RT::SystemUser); +$acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' ); +$acl->LimitToObject( $RT::System ); +while( my $ace = $acl->Next ) { + $ace->Delete; +} + +# create new queue to be sure we do not mess with rights +my $queue = RT::Queue->new($RT::SystemUser); +my ($queue_id) = $queue->Create( Name => 'watcher tests '.$$); +ok( $queue_id, 'queue created for watcher tests' ); + +# new privileged user to check rights +my $user = RT::User->new( $RT::SystemUser ); +my ($user_id) = $user->Create( Name => 'watcher'.$$, + EmailAddress => "watcher$$".'@localhost', + Privileged => 1, + Password => 'qwe123', + ); +my $cu= RT::CurrentUser->new($user); + +# make sure user can see tickets in the queue +my $principal = $user->PrincipalObj; +ok( $principal, "principal loaded" ); +$principal->GrantRight( Right => 'ShowTicket', Object => $queue ); +$principal->GrantRight( Right => 'SeeQueue' , Object => $queue ); + +ok( $user->HasRight( Right => 'SeeQueue', Object => $queue ), "user can see queue" ); +ok( $user->HasRight( Right => 'ShowTicket', Object => $queue ), "user can show queue tickets" ); +ok( !$user->HasRight( Right => 'ModifyTicket', Object => $queue ), "user can't modify queue tickets" ); +ok( !$user->HasRight( Right => 'Watch', Object => $queue ), "user can't watch queue tickets" ); + +my $ticket = RT::Ticket->new( $RT::SystemUser ); +my ($rv, $msg) = $ticket->Create( Subject => 'watcher tests', Queue => $queue->Name ); +ok( $ticket->id, "ticket created" ); + +my $ticket2 = RT::Ticket->new( $cu ); +$ticket2->Load( $ticket->id ); +ok( $ticket2->Subject, "ticket load by user" ); + +# user can add self to ticket only after getting Watch right +($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId ); +ok( !$rv, "user can't add self as Cc" ); +($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId ); +ok( !$rv, "user can't add self as Requestor" ); +$principal->GrantRight( Right => 'Watch' , Object => $queue ); +ok( $user->HasRight( Right => 'Watch', Object => $queue ), "user can watch queue tickets" ); +($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId ); +ok( $rv, "user can add self as Cc by PrincipalId" ); +($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId ); +ok( $rv, "user can add self as Requestor by PrincipalId" ); + +# remove user and try adding with Email address +($rv, $msg) = $ticket->DeleteWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId ); +ok( $rv, "watcher removed by PrincipalId" ); +($rv, $msg) = $ticket->DeleteWatcher( Type => 'Requestor', Email => $user->EmailAddress ); +ok( $rv, "watcher removed by Email" ); + +($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', Email => $user->EmailAddress ); +ok( $rv, "user can add self as Cc by Email" ); +($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', Email => $user->EmailAddress ); +ok( $rv, "user can add self as Requestor by Email" ); + +# remove user and try adding by username +# This worked in 3.6 and is a regression in 3.8 +($rv, $msg) = $ticket->DeleteWatcher( Type => 'Cc', Email => $user->EmailAddress ); +ok( $rv, "watcher removed by Email" ); +($rv, $msg) = $ticket->DeleteWatcher( Type => 'Requestor', Email => $user->EmailAddress ); +ok( $rv, "watcher removed by Email" ); + +($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', Email => $user->Name ); +ok( $rv, "user can add self as Cc by username" ); +($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', Email => $user->Name ); +ok( $rv, "user can add self as Requestor by username" ); + +# Queue watcher tests +$principal->RevokeRight( Right => 'Watch' , Object => $queue ); +ok( !$user->HasRight( Right => 'Watch', Object => $queue ), "user queue watch right revoked" ); + +my $queue2 = RT::Queue->new( $cu ); +($rv, $msg) = $queue2->Load( $queue->id ); +ok( $rv, "user loaded queue" ); + +# user can add self to queue only after getting Watch right +($rv, $msg) = $queue2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId ); +ok( !$rv, "user can't add self as Cc" ); +($rv, $msg) = $queue2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId ); +ok( !$rv, "user can't add self as Requestor" ); +$principal->GrantRight( Right => 'Watch' , Object => $queue ); +ok( $user->HasRight( Right => 'Watch', Object => $queue ), "user can watch queue queues" ); +($rv, $msg) = $queue2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId ); +ok( $rv, "user can add self as Cc by PrincipalId" ); +($rv, $msg) = $queue2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId ); +ok( $rv, "user can add self as Requestor by PrincipalId" ); + +# remove user and try adding with Email address +($rv, $msg) = $queue->DeleteWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId ); +ok( $rv, "watcher removed by PrincipalId" ); +($rv, $msg) = $queue->DeleteWatcher( Type => 'Requestor', Email => $user->EmailAddress ); +ok( $rv, "watcher removed by Email" ); + +($rv, $msg) = $queue2->AddWatcher( Type => 'Cc', Email => $user->EmailAddress ); +ok( $rv, "user can add self as Cc by Email" ); +($rv, $msg) = $queue2->AddWatcher( Type => 'Requestor', Email => $user->EmailAddress ); +ok( $rv, "user can add self as Requestor by Email" ); + diff --git a/rt/t/ticket/badlinks.t b/rt/t/ticket/badlinks.t new file mode 100644 index 000000000..408e6b67c --- /dev/null +++ b/rt/t/ticket/badlinks.t @@ -0,0 +1,38 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use RT::Test tests => 12; + +my ($baseurl, $m) = RT::Test->started_ok; +ok($m->login, "Logged in"); + +my $queue = RT::Test->load_or_create_queue(Name => 'General'); +ok($queue->Id, "loaded the General queue"); + +my $ticket = RT::Ticket->new($RT::SystemUser); +my ($tid, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Subject => 'test links', + ); +ok $tid, 'created a ticket #'. $tid or diag "error: $msg"; + +$m->goto_ticket($tid); + +$m->follow_link_ok( { text => 'Links' }, "Followed link to Links" ); + +ok $m->form_with_fields("$tid-DependsOn"), "found the form"; +my $not_a_ticket_url = "http://example.com/path/to/nowhere"; +$m->field("$tid-DependsOn", $not_a_ticket_url); +$m->field("DependsOn-$tid", $not_a_ticket_url); +$m->field("$tid-MemberOf", $not_a_ticket_url); +$m->field("MemberOf-$tid", $not_a_ticket_url); +$m->field("$tid-RefersTo", $not_a_ticket_url); +$m->field("RefersTo-$tid", $not_a_ticket_url); +$m->submit; + +foreach my $type ("depends on", "member of", "refers to") { + $m->content_like(qr/$type.+$not_a_ticket_url/,"base for $type"); + $m->content_like(qr/$not_a_ticket_url.+$type/,"target for $type"); +} + +$m->goto_ticket($tid); diff --git a/rt/t/ticket/batch-upload-csv.t b/rt/t/ticket/batch-upload-csv.t new file mode 100644 index 000000000..41dc78696 --- /dev/null +++ b/rt/t/ticket/batch-upload-csv.t @@ -0,0 +1,48 @@ +#!/usr/bin/perl -w +use strict; use warnings; + +use RT::Test tests => 12; +use_ok('RT'); + +use_ok('RT::Action::CreateTickets'); + +my $QUEUE = 'uploadtest-'.$$; + +my $queue_obj = RT::Queue->new($RT::SystemUser); +$queue_obj->Create(Name => $QUEUE); + +my $cf = RT::CustomField->new($RT::SystemUser); +my ($val,$msg) = $cf->Create(Name => 'Work Package-'.$$, Type => 'Freeform', LookupType => RT::Ticket->CustomFieldLookupType, MaxValues => 1); +ok($cf->id); +ok($val,$msg); +($val, $msg) = $cf->AddToObject($queue_obj); +ok($val,$msg); +ok($queue_obj->TicketCustomFields()->Count, "We have a custom field, at least"); + + +my $data = <<EOF; +id,Queue,Subject,Status,Requestor,@{[$cf->Name]} +create-1,$QUEUE,hi,new,root,2.0 +create-2,$QUEUE,hello,new,root,3.0 +EOF + +my $action = RT::Action::CreateTickets->new(CurrentUser => RT::CurrentUser->new('root')); +ok ($action->CurrentUser->id , "WE have a current user"); + +$action->Parse(Content => $data); +my @results = $action->CreateByTemplate(); + +my $tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL ("Queue = '". $QUEUE."'"); +$tix->OrderBy( FIELD => 'id', ORDER => 'ASC' ); +is($tix->Count, 2, '2 tickets'); + +my $first = $tix->First(); + +is($first->Subject(), 'hi'); +is($first->FirstCustomFieldValue($cf->id), '2.0'); + +my $second = $tix->Next; +is($second->Subject(), 'hello'); +is($second->FirstCustomFieldValue($cf->id), '3.0'); +1; diff --git a/rt/t/ticket/cfsort-freeform-multiple.t b/rt/t/ticket/cfsort-freeform-multiple.t new file mode 100644 index 000000000..f8f5950ef --- /dev/null +++ b/rt/t/ticket/cfsort-freeform-multiple.t @@ -0,0 +1,137 @@ +#!/usr/bin/perl + +use RT::Test tests => 24; + +use strict; +use warnings; + +use RT::Tickets; +use RT::Queue; +use RT::CustomField; + +# Test Sorting by custom fields. + +diag "Create a queue to test with." if $ENV{TEST_VERBOSE}; +my $queue_name = "CFSortQueue-$$"; +my $queue; +{ + $queue = RT::Queue->new( $RT::SystemUser ); + my ($ret, $msg) = $queue->Create( + Name => $queue_name, + Description => 'queue for custom field sort testing' + ); + ok($ret, "$queue_name - test queue creation. $msg"); +} + +diag "create a CF\n" if $ENV{TEST_VERBOSE}; +my $cf_name = "Order$$"; +my $cf; +{ + $cf = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $cf->Create( + Name => $cf_name, + Queue => $queue->id, + Type => 'FreeformMultiple', + ); + ok($ret, "Custom Field Order created"); +} + +my ($total, @data, @tickets, @test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + @data = sort { rand(100) <=> rand(100) } @data; + while (@data) { + my $t = RT::Ticket->new($RT::SystemUser); + my %args = %{ shift(@data) }; + my @values = (); + if ( exists $args{'CF'} && ref $args{'CF'} ) { + @values = @{ delete $args{'CF'} }; + } elsif ( exists $args{'CF'} ) { + @values = (delete $args{'CF'}); + } + $args{ 'CustomField-'. $cf->id } = \@values + if @values; + my $subject = join(",", sort @values) || '-'; + my ( $id, undef $msg ) = $t->Create( + %args, + Queue => $queue->id, + Subject => $subject, + ); + ok( $id, "ticket created" ) or diag("error: $msg"); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $test ( @test ) { + my $query = join " AND ", map "( $_ )", grep defined && length, + $query_prefix, $test->{'Query'}; + + foreach my $order (qw(ASC DESC)) { + my $error = 0; + my $tix = RT::Tickets->new( $RT::SystemUser ); + $tix->FromSQL( $query ); + $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order ); + + ok($tix->Count, "found ticket(s)") + or $error = 1; + + my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz'); + my $last_id = $tix->Last->id; + while ( my $t = $tix->Next ) { + my $tmp; + next if $t->id == $last_id and $t->Subject eq "-"; # Nulls are allowed to come last, in Pg + + if ( $order eq 'ASC' ) { + $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]); + } else { + $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]); + } + if ( $tmp > 0 ) { + $order_ok = 0; last; + } + $last = $t->Subject; + } + + ok( $order_ok, "$order order of tickets is good" ) + or $error = 1; + + if ( $error ) { + diag "Wrong SQL query:". $tix->BuildSelectQuery; + $tix->GotoFirstItem; + while ( my $t = $tix->Next ) { + diag sprintf "%02d - %s", $t->id, $t->Subject; + } + } + } + } +} + +@data = ( + { }, + { CF => ['b', 'd'] }, + { CF => ['a', 'c'] }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "CF.{$cf_name}" }, + { Order => "CF.$queue_name.{$cf_name}" }, +); +run_tests(); + +@data = ( + { CF => ['m', 'a'] }, + { CF => ['m'] }, + { CF => ['m', 'o'] }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "CF.{$cf_name}", Query => "CF.{$cf_name} = 'm'" }, + { Order => "CF.$queue_name.{$cf_name}", Query => "CF.{$cf_name} = 'm'" }, +); +run_tests(); + diff --git a/rt/t/ticket/cfsort-freeform-single.t b/rt/t/ticket/cfsort-freeform-single.t new file mode 100644 index 000000000..f1f506bea --- /dev/null +++ b/rt/t/ticket/cfsort-freeform-single.t @@ -0,0 +1,191 @@ +#!/usr/bin/perl + +use RT::Test tests => 57; + +use strict; +use warnings; + +use RT::Tickets; +use RT::Queue; +use RT::CustomField; + +# Test Sorting by FreeformSingle custom field. + +diag "Create a queue to test with." if $ENV{TEST_VERBOSE}; +my $queue_name = "CFSortQueue-$$"; +my $queue; +{ + $queue = RT::Queue->new( $RT::SystemUser ); + my ($ret, $msg) = $queue->Create( + Name => $queue_name, + Description => 'queue for custom field sort testing' + ); + ok($ret, "$queue test queue creation. $msg"); +} + +# CFs for testing, later we create another one +my %CF; +my $cf_name; + +diag "create a CF\n" if $ENV{TEST_VERBOSE}; +{ + $cf_name = $CF{'CF'}{'name'} = "Order$$"; + $CF{'CF'}{'obj'} = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $CF{'CF'}{'obj'}->Create( + Name => $CF{'CF'}{'name'}, + Queue => $queue->id, + Type => 'FreeformSingle', + ); + ok($ret, "Custom Field $CF{'CF'}{'name'} created"); +} + +my ($total, @data, @tickets, @test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + @data = sort { rand(100) <=> rand(100) } @data; + while (@data) { + my $t = RT::Ticket->new($RT::SystemUser); + my %args = %{ shift(@data) }; + + my $subject = '-'; + foreach my $e ( grep exists $CF{$_} && defined $CF{$_}, keys %args ) { + my @values = (); + if ( ref $args{ $e } ) { + @values = @{ delete $args{ $e } }; + } else { + @values = (delete $args{ $e }); + } + $args{ 'CustomField-'. $CF{ $e }{'obj'}->id } = \@values + if @values; + $subject = join(",", sort @values) || '-' + if $e eq 'CF'; + } + + my ( $id, undef $msg ) = $t->Create( + %args, + Queue => $queue->id, + Subject => $subject, + ); + ok( $id, "ticket created" ) or diag("error: $msg"); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $test ( @test ) { + my $query = join " AND ", map "( $_ )", grep defined && length, + $query_prefix, $test->{'Query'}; + + foreach my $order (qw(ASC DESC)) { + my $error = 0; + my $tix = RT::Tickets->new( $RT::SystemUser ); + $tix->FromSQL( $query ); + $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order ); + + ok($tix->Count, "found ticket(s)") + or $error = 1; + + my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz'); + my $last_id = $tix->Last->id; + while ( my $t = $tix->Next ) { + my $tmp; + next if $t->id == $last_id and $t->Subject eq "-"; # Nulls are allowed to come last, in Pg + + if ( $order eq 'ASC' ) { + $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]); + } else { + $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]); + } + if ( $tmp > 0 ) { + $order_ok = 0; last; + } + $last = $t->Subject; + } + + ok( $order_ok, "$order order of tickets is good" ) + or $error = 1; + + if ( $error ) { + diag "Wrong SQL query:". $tix->BuildSelectQuery; + $tix->GotoFirstItem; + while ( my $t = $tix->Next ) { + diag sprintf "%02d - %s", $t->id, $t->Subject; + } + } + } + } +} + +@data = ( + { }, + { CF => 'a' }, + { CF => 'b' }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "CF.{$cf_name}" }, + { Order => "CF.$queue_name.{$cf_name}" }, +); +run_tests(); + +@data = ( + { }, + { CF => 'aa' }, + { CF => 'ab' }, +); +@tickets = add_tix_from_data(); +@test = ( + { Query => "CF.{$cf_name} LIKE 'a'", Order => "CF.{$cf_name}" }, + { Query => "CF.{$cf_name} LIKE 'a'", Order => "CF.$queue_name.{$cf_name}" }, +); +run_tests(); + +@data = ( + { Subject => '-', }, + { Subject => 'a', CF => 'a' }, + { Subject => 'b', CF => 'b' }, + { Subject => 'c', CF => 'c' }, +); +@tickets = add_tix_from_data(); +@test = ( + { Query => "CF.{$cf_name} != 'c'", Order => "CF.{$cf_name}" }, + { Query => "CF.{$cf_name} != 'c'", Order => "CF.$queue_name.{$cf_name}" }, +); +run_tests(); + + + +diag "create another CF\n" if $ENV{TEST_VERBOSE}; +{ + $CF{'AnotherCF'}{'name'} = "OrderAnother$$"; + $CF{'AnotherCF'}{'obj'} = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $CF{'AnotherCF'}{'obj'}->Create( + Name => $CF{'AnotherCF'}{'name'}, + Queue => $queue->id, + Type => 'FreeformSingle', + ); + ok($ret, "Custom Field $CF{'AnotherCF'}{'name'} created"); +} + +# test that order is not affect by other fields (had such problem) +@data = ( + { Subject => '-', }, + { Subject => 'a', CF => 'a', AnotherCF => 'za' }, + { Subject => 'b', CF => 'b', AnotherCF => 'ya' }, + { Subject => 'c', CF => 'c', AnotherCF => 'xa' }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "CF.{$cf_name}" }, + { Order => "CF.$queue_name.{$cf_name}" }, + { Query => "CF.{$cf_name} != 'c'", Order => "CF.{$cf_name}" }, + { Query => "CF.{$cf_name} != 'c'", Order => "CF.$queue_name.{$cf_name}" }, +); +run_tests(); + + + diff --git a/rt/t/ticket/deferred_owner.t b/rt/t/ticket/deferred_owner.t new file mode 100644 index 000000000..40172caf9 --- /dev/null +++ b/rt/t/ticket/deferred_owner.t @@ -0,0 +1,120 @@ + +use strict; +use warnings; + +use RT::Test tests => 18; +use_ok('RT'); +use_ok('RT::Ticket'); +use Test::Warn; + + +my $tester = RT::Test->load_or_create_user( + EmailAddress => 'tester@localhost', +); +ok $tester && $tester->id, 'loaded or created user'; + +my $queue = RT::Test->load_or_create_queue( Name => 'General' ); +ok $queue && $queue->id, 'loaded or created queue'; + +my $owner_role_group = RT::Group->new( $RT::SystemUser ); +$owner_role_group->LoadQueueRoleGroup( Type => 'Owner', Queue => $queue->id ); +ok $owner_role_group->id, 'loaded owners role group of the queue'; + +diag "check that deffering owner doesn't regress" if $ENV{'TEST_VERBOSE'}; +{ + RT::Test->set_rights( + { Principal => $tester->PrincipalObj, + Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket)], + }, + { Principal => $owner_role_group->PrincipalObj, + Object => $queue, + Right => [qw(ModifyTicket)], + }, + ); + my $ticket = RT::Ticket->new( $tester ); + # tester is owner, owner has right to modify owned tickets, + # this right is required to set somebody as AdminCc + my ($tid, $txn_id, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $tester->id, + AdminCc => 'root@localhost', + ); + diag $msg if $msg && $ENV{'TEST_VERBOSE'}; + ok $tid, "created a ticket"; + is $ticket->Owner, $tester->id, 'correct owner'; + like $ticket->AdminCcAddresses, qr/root\@localhost/, 'root is there'; +} + +diag "check that previous trick doesn't work without sufficient rights" + if $ENV{'TEST_VERBOSE'}; +{ + RT::Test->set_rights( + { Principal => $tester->PrincipalObj, + Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket)], + }, + ); + my $ticket = RT::Ticket->new( $tester ); + # tester is owner, owner has right to modify owned tickets, + # this right is required to set somebody as AdminCc + my ($tid, $txn_id, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $tester->id, + AdminCc => 'root@localhost', + ); + diag $msg if $msg && $ENV{'TEST_VERBOSE'}; + ok $tid, "created a ticket"; + is $ticket->Owner, $tester->id, 'correct owner'; + unlike $ticket->AdminCcAddresses, qr/root\@localhost/, 'root is there'; +} + +diag "check that deffering owner really works" if $ENV{'TEST_VERBOSE'}; +{ + RT::Test->set_rights( + { Principal => $tester->PrincipalObj, + Right => [qw(SeeQueue ShowTicket CreateTicket)], + }, + { Principal => $queue->Cc->PrincipalObj, + Object => $queue, + Right => [qw(OwnTicket TakeTicket)], + }, + ); + my $ticket = RT::Ticket->new( $tester ); + # set tester as Cc, Cc role group has right to own and take tickets + my ($tid, $txn_id, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $tester->id, + Cc => 'tester@localhost', + ); + diag $msg if $msg && $ENV{'TEST_VERBOSE'}; + ok $tid, "created a ticket"; + like $ticket->CcAddresses, qr/tester\@localhost/, 'tester is in the cc list'; + is $ticket->Owner, $tester->id, 'tester is also owner'; +} + +diag "check that deffering doesn't work without correct rights" if $ENV{'TEST_VERBOSE'}; +{ + RT::Test->set_rights( + { Principal => $tester->PrincipalObj, + Right => [qw(SeeQueue ShowTicket CreateTicket)], + }, + ); + + my $ticket = RT::Ticket->new( $tester ); + # set tester as Cc, Cc role group has right to own and take tickets + my ($tid, $txn_id, $msg); + warning_like { + ($tid, $txn_id, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $tester->id, + Cc => 'tester@localhost', + ); + } qr/User .* was proposed as a ticket owner but has no rights to own tickets in General/; + + diag $msg if $msg && $ENV{'TEST_VERBOSE'}; + ok $tid, "created a ticket"; + like $ticket->CcAddresses, qr/tester\@localhost/, 'tester is in the cc list'; + isnt $ticket->Owner, $tester->id, 'tester is also owner'; +} + + + diff --git a/rt/t/ticket/link_search.t b/rt/t/ticket/link_search.t new file mode 100644 index 000000000..1bf7dc6dc --- /dev/null +++ b/rt/t/ticket/link_search.t @@ -0,0 +1,246 @@ +#!/usr/bin/perl -w + +use strict; +use RT; + +# Load the config file +use RT::Test tests => 63; + +#Connect to the database and get RT::SystemUser and RT::Nobody loaded + + +#Get the current user all loaded +my $CurrentUser = $RT::SystemUser; + +my $queue = new RT::Queue($CurrentUser); +$queue->Load('General') || Abort(loc("Queue could not be loaded.")); + +my $child_ticket = new RT::Ticket( $CurrentUser ); +my ($childid) = $child_ticket->Create( + Subject => 'test child', + Queue => $queue->Id, +); +ok($childid, "We created a child ticket"); + +my $parent_ticket = new RT::Ticket( $CurrentUser ); +my ($parentid) = $parent_ticket->Create( + Subject => 'test parent', + Children => [ $childid ], + Queue => $queue->Id, +); +ok($parentid, "We created a parent ticket"); + + +my $Collection = RT::Tickets->new($CurrentUser); +$Collection->LimitMemberOf( $parentid ); +is($Collection->Count,1, "We found only one result"); +ok($Collection->First); +is($Collection->First->id, $childid, "We found the collection of all children of $parentid with Limit"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL("MemberOf = $parentid"); +is($Collection->Count, 1, "We found only one result"); +ok($Collection->First); +is($Collection->First->id, $childid, "We found the collection of all children of $parentid with TicketSQL"); + + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->LimitHasMember ($childid); +is($Collection->Count,1, "We found only one result"); +ok($Collection->First); +is($Collection->First->id, $parentid, "We found the collection of all parents of $childid with Limit"); + + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL("HasMember = $childid"); +is($Collection->Count,1, "We found only one result"); +ok($Collection->First); +is($Collection->First->id, $parentid, "We found the collection of all parents of $childid with TicketSQL"); + + +# Now we find a collection of all the tickets which have no members. they should have no children. +$Collection = RT::Tickets->new($CurrentUser); +$Collection->LimitHasMember(''); +# must contain child; must not contain parent +my %has; +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( $has{$childid}, "The collection has our child - $childid"); +ok( !$has{$parentid}, "The collection doesn't have our parent - $parentid"); + + +# Now we find a collection of all the tickets which are not members of anything. they should have no parents. +$Collection = RT::Tickets->new($CurrentUser); +$Collection->LimitMemberOf(''); +# must contain parent; must not contain child +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok ($has{$parentid} , "The collection has our parent - $parentid"); +ok( !$has{$childid}, "The collection doesn't have our child - $childid"); + + +# Do it all over with TicketSQL +# + + + +# Now we find a collection of all the tickets which have no members. they should have no children. +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL ("HasMember IS NULL"); +# must contain parent; must not contain child +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( !$has{$parentid}, "The collection doesn't have our parent - $parentid"); +ok( $has{$childid}, "The collection has our child - $childid"); + + +# Now we find a collection of all the tickets which have no members. they should have no children. +# Alternate syntax +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL("HasMember = ''"); +# must contain parent; must not contain child +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( !$has{$parentid}, "The collection doesn't have our parent - $parentid"); +ok( $has{$childid}, "The collection has our child - $childid"); + + +# Now we find a collection of all the tickets which are not members of anything. they should have no parents. +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL("MemberOf IS NULL"); +# must not contain parent; must contain parent +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( $has{$parentid}, "The collection has our parent - $parentid"); +ok( !$has{$childid}, "The collection doesn't have our child - $childid"); + + +# Now we find a collection of all the tickets which are not members of anything. they should have no parents. +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL("MemberOf = ''"); +# must not contain parent; must contain parent +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( $has{$parentid}, "The collection has our parent - $parentid"); +ok( !$has{$childid}, "The collection doesn't have our child - $childid"); + + +# Now we find a collection of all the tickets which are not members of the parent ticket +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL("MemberOf != $parentid"); +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( $has{$parentid}, "The collection has our parent - $parentid"); +ok( !$has{$childid}, "The collection doesn't have our child - $childid"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->LimitMemberOf($parentid, OPERATOR => '!='); +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( $has{$parentid}, "The collection has our parent - $parentid"); +ok( !$has{$childid}, "The collection doesn't have our child - $childid"); + +my $grand_child_ticket = new RT::Ticket( $CurrentUser ); +my ($grand_childid) = $child_ticket->Create( + Subject => 'test child', + Queue => $queue->Id, + MemberOf => $childid, +); +ok($childid, "We created a grand child ticket"); + +my $unlinked_ticket = new RT::Ticket( $CurrentUser ); +my ($unlinked_id) = $child_ticket->Create( + Subject => 'test unlinked', + Queue => $queue->Id, +); +ok($unlinked_id, "We created a grand child ticket"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL( "LinkedTo = $childid" ); +is($Collection->Count,1, "We found only one result"); +ok($Collection->First); +is($Collection->First->id, $grand_childid, "We found all tickets linked to ticket #$childid"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL( "LinkedFrom = $childid" ); +is($Collection->Count,1, "We found only one result"); +ok($Collection->First); +is($Collection->First->id, $parentid, "We found all tickets linked from ticket #$childid"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL( "LinkedTo IS NULL" ); +ok($Collection->Count, "Result is set is not empty"); +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( $has{$parentid}, "parent is in collection"); +ok( $has{$unlinked_id}, "unlinked is in collection"); +ok( !$has{$childid}, "child is NOT in collection"); +ok( !$has{$grand_childid}, "grand child too is not in collection"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL( "LinkedTo IS NOT NULL" ); +ok($Collection->Count, "Result set is not empty"); +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( !$has{$parentid}, "The collection has no our parent - $parentid"); +ok( !$has{$unlinked_id}, "unlinked is not in collection"); +ok( $has{$childid}, "The collection have our child - $childid"); +ok( $has{$grand_childid}, "The collection have our grand child - $grand_childid"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL( "LinkedFrom IS NULL" ); +ok($Collection->Count, "Result is set is not empty"); +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( !$has{$parentid}, "parent is NOT in collection"); +ok( !$has{$childid}, "child is NOT in collection"); +ok( $has{$grand_childid}, "grand child is in collection"); +ok( $has{$unlinked_id}, "unlinked is in collection"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL( "LinkedFrom IS NOT NULL" ); +ok($Collection->Count, "Result set is not empty"); +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( $has{$parentid}, "The collection has our parent - $parentid"); +ok( $has{$childid}, "The collection have our child - $childid"); +ok( !$has{$grand_childid}, "The collection have no our grand child - $grand_childid"); +ok( !$has{$unlinked_id}, "unlinked is not in collection"); + +$Collection = RT::Tickets->new($CurrentUser); +$Collection->FromSQL( "Linked = $childid" ); +is($Collection->Count, 2, "We found two tickets: parent and child"); +%has = (); +while (my $t = $Collection->Next) { + ++$has{$t->id}; +} +ok( !$has{$childid}, "Ticket is not linked to itself"); +ok( $has{$parentid}, "The collection has our parent"); +ok( $has{$grand_childid}, "The collection have our child"); +ok( !$has{$unlinked_id}, "unlinked is not in collection"); + + +1; diff --git a/rt/t/ticket/linking.t b/rt/t/ticket/linking.t new file mode 100644 index 000000000..2ea3d58da --- /dev/null +++ b/rt/t/ticket/linking.t @@ -0,0 +1,385 @@ + +use strict; +use warnings; + +use RT::Test tests => '101'; +use_ok('RT'); +use_ok('RT::Ticket'); +use_ok('RT::ScripConditions'); +use_ok('RT::ScripActions'); +use_ok('RT::Template'); +use_ok('RT::Scrips'); +use_ok('RT::Scrip'); + + +use File::Temp qw/tempfile/; +my ($fh, $filename) = tempfile( UNLINK => 1, SUFFIX => '.rt'); +my $link_scrips_orig = RT->Config->Get( 'LinkTransactionsRun1Scrip' ); +RT->Config->Set( 'LinkTransactionsRun1Scrip', 1 ); + +my $link_acl_checks_orig = RT->Config->Get( 'StrictLinkACL' ); +RT->Config->Set( 'StrictLinkACL', 1); + +my $condition = RT::ScripCondition->new( $RT::SystemUser ); +$condition->Load('User Defined'); +ok($condition->id); +my $action = RT::ScripAction->new( $RT::SystemUser ); +$action->Load('User Defined'); +ok($action->id); +my $template = RT::Template->new( $RT::SystemUser ); +$template->Load('Blank'); +ok($template->id); + +my $q1 = RT::Queue->new($RT::SystemUser); +my ($id,$msg) = $q1->Create(Name => "LinkTest1.$$"); +ok ($id,$msg); +my $q2 = RT::Queue->new($RT::SystemUser); +($id,$msg) = $q2->Create(Name => "LinkTest2.$$"); +ok ($id,$msg); + +my $commit_code = <<END; +open my \$file, "<$filename" or die "couldn't open $filename"; +my \$data = <\$file>; +chomp \$data; +\$data += 0; +close \$file; +\$RT::Logger->debug("Data is \$data"); + +open \$file, ">$filename" or die "couldn't open $filename"; +if (\$self->TransactionObj->Type eq 'AddLink') { + \$RT::Logger->debug("AddLink"); + print \$file \$data+1, "\n"; +} +elsif (\$self->TransactionObj->Type eq 'DeleteLink') { + \$RT::Logger->debug("DeleteLink"); + print \$file \$data-1, "\n"; +} +else { + \$RT::Logger->error("THIS SHOULDN'T HAPPEN"); + print \$file "666\n"; +} +close \$file; +1; +END + +my $Scrips = RT::Scrips->new( $RT::SystemUser ); +$Scrips->UnLimit; +while ( my $Scrip = $Scrips->Next ) { + $Scrip->Delete if $Scrip->Description and $Scrip->Description =~ /Add or Delete Link \d+/; +} + + +my $scrip = RT::Scrip->new($RT::SystemUser); +($id,$msg) = $scrip->Create( Description => "Add or Delete Link $$", + ScripCondition => $condition->id, + ScripAction => $action->id, + Template => $template->id, + Stage => 'TransactionCreate', + Queue => 0, + CustomIsApplicableCode => '$self->TransactionObj->Type =~ /(Add|Delete)Link/;', + CustomPrepareCode => '1;', + CustomCommitCode => $commit_code, + ); +ok($id, "Scrip created"); + +my $u1 = RT::User->new($RT::SystemUser); +($id,$msg) = $u1->Create(Name => "LinkTestUser.$$"); +ok ($id,$msg); + +# grant ShowTicket right to allow count transactions +($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q1, Right => 'ShowTicket'); +ok ($id,$msg); +($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q2, Right => 'ShowTicket'); +ok ($id,$msg); +($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q1, Right => 'CreateTicket'); +ok ($id,$msg); + +my $creator = RT::CurrentUser->new($u1->id); + +diag('Create tickets without rights to link') if $ENV{'TEST_VERBOSE'}; +{ + # on q2 we have no rights, yet + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id ); + ok($id,$msg); + my $child = RT::Ticket->new( $creator ); + ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id, MemberOf => $parent->id ); + ok($id,$msg); + $child->CurrentUser( $RT::SystemUser ); + is($child->_Links('Base')->Count, 0, 'link was not created, no permissions'); + is($child->_Links('Target')->Count, 0, 'link was not create, no permissions'); +} + +diag('Create tickets with rights checks on one end of a link') if $ENV{'TEST_VERBOSE'}; +{ + # on q2 we have no rights, but use checking one only on thing + RT->Config->Set( StrictLinkACL => 0 ); + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id ); + ok($id,$msg); + my $child = RT::Ticket->new( $creator ); + ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id, MemberOf => $parent->id ); + ok($id,$msg); + $child->CurrentUser( $RT::SystemUser ); + is($child->_Links('Base')->Count, 1, 'link was created'); + is($child->_Links('Target')->Count, 0, 'link was created only one'); + # no scrip run on second ticket accroding to config option + is(link_count($filename), undef, "scrips ok"); + RT->Config->Set( StrictLinkACL => 1 ); +} + +($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q1, Right => 'ModifyTicket'); +ok ($id,$msg); + +diag('try to add link without rights') if $ENV{'TEST_VERBOSE'}; +{ + # on q2 we have no rights, yet + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id ); + ok($id,$msg); + my $child = RT::Ticket->new( $creator ); + ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id ); + ok($id,$msg); + ($id, $msg) = $child->AddLink(Type => 'MemberOf', Target => $parent->id); + ok(!$id, $msg); + is(link_count($filename), undef, "scrips ok"); + $child->CurrentUser( $RT::SystemUser ); + is($child->_Links('Base')->Count, 0, 'link was not created, no permissions'); + is($child->_Links('Target')->Count, 0, 'link was not create, no permissions'); +} + +diag('add link with rights only on base') if $ENV{'TEST_VERBOSE'}; +{ + # on q2 we have no rights, but use checking one only on thing + RT->Config->Set( StrictLinkACL => 0 ); + my $parent = RT::Ticket->new( $RT::SystemUser ); + my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id ); + ok($id,$msg); + my $child = RT::Ticket->new( $creator ); + ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id ); + ok($id,$msg); + ($id, $msg) = $child->AddLink(Type => 'MemberOf', Target => $parent->id); + ok($id, $msg); + is(link_count($filename), 1, "scrips ok"); + $child->CurrentUser( $RT::SystemUser ); + is($child->_Links('Base')->Count, 1, 'link was created'); + is($child->_Links('Target')->Count, 0, 'link was created only one'); + $child->CurrentUser( $creator ); + + # turn off feature and try to delete link, we should fail + RT->Config->Set( StrictLinkACL => 1 ); + ($id, $msg) = $child->AddLink(Type => 'MemberOf', Target => $parent->id); + ok(!$id, $msg); + is(link_count($filename), 1, "scrips ok"); + $child->CurrentUser( $RT::SystemUser ); + $child->_Links('Base')->_DoCount; + is($child->_Links('Base')->Count, 1, 'link was not deleted'); + $child->CurrentUser( $creator ); + + # try to delete link, we should success as feature is active + RT->Config->Set( StrictLinkACL => 0 ); + ($id, $msg) = $child->DeleteLink(Type => 'MemberOf', Target => $parent->id); + ok($id, $msg); + is(link_count($filename), 0, "scrips ok"); + $child->CurrentUser( $RT::SystemUser ); + $child->_Links('Base')->_DoCount; + is($child->_Links('Base')->Count, 0, 'link was deleted'); + RT->Config->Set( StrictLinkACL => 1 ); +} + +my $tid; +my $ticket = RT::Ticket->new( $creator); +ok($ticket->isa('RT::Ticket')); +($id,$tid, $msg) = $ticket->Create(Subject => 'Link test 1', Queue => $q1->id); +ok ($id,$msg); + +diag('try link to itself') if $ENV{'TEST_VERBOSE'}; +{ + my ($id, $msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket->id); + ok(!$id, $msg); + is(link_count($filename), 0, "scrips ok"); +} + +my $ticket2 = RT::Ticket->new($RT::SystemUser); +($id, $tid, $msg) = $ticket2->Create(Subject => 'Link test 2', Queue => $q2->id); +ok ($id, $msg); +($id,$msg) =$ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id); +ok(!$id,$msg); +is(link_count($filename), 0, "scrips ok"); + +($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q2, Right => 'CreateTicket'); +ok ($id,$msg); +($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q2, Right => 'ModifyTicket'); +ok ($id,$msg); +($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id); +ok($id,$msg); +is(link_count($filename), 1, "scrips ok"); +($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => -1); +ok(!$id,$msg); +($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id); +ok($id,$msg); +is(link_count($filename), 1, "scrips ok"); + +my $transactions = $ticket2->Transactions; +$transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); +is( $transactions->Count, 1, "Transaction found in other ticket" ); +is( $transactions->First->Field , 'ReferredToBy'); +is( $transactions->First->NewValue , $ticket->URI ); + +($id,$msg) = $ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id); +ok($id,$msg); +is(link_count($filename), 0, "scrips ok"); +$transactions = $ticket2->Transactions; +$transactions->Limit( FIELD => 'Type', VALUE => 'DeleteLink' ); +is( $transactions->Count, 1, "Transaction found in other ticket" ); +is( $transactions->First->Field , 'ReferredToBy'); +is( $transactions->First->OldValue , $ticket->URI ); + +RT->Config->Set( LinkTransactionsRun1Scrip => 0 ); + +($id,$msg) =$ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id); +ok($id,$msg); +is(link_count($filename), 2, "scrips ok"); +($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id); +ok($id,$msg); +is(link_count($filename), 0, "scrips ok"); + +# tests for silent behaviour +($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id, Silent => 1); +ok($id,$msg); +is(link_count($filename), 0, "scrips ok"); +{ + my $transactions = $ticket->Transactions; + $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); + is( $transactions->Count, 2, "Still two txns on the base" ); + + $transactions = $ticket2->Transactions; + $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); + is( $transactions->Count, 2, "Still two txns on the target" ); + +} +($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id, Silent => 1); +ok($id,$msg); +is(link_count($filename), 0, "scrips ok"); + +($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id, SilentBase => 1); +ok($id,$msg); +is(link_count($filename), 1, "scrips ok"); +{ + my $transactions = $ticket->Transactions; + $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); + is( $transactions->Count, 2, "still five txn on the base" ); + + $transactions = $ticket2->Transactions; + $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); + is( $transactions->Count, 3, "+1 txn on the target" ); + +} +($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id, SilentBase => 1); +ok($id,$msg); +is(link_count($filename), 0, "scrips ok"); + +($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id, SilentTarget => 1); +ok($id,$msg); +is(link_count($filename), 1, "scrips ok"); +{ + my $transactions = $ticket->Transactions; + $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); + is( $transactions->Count, 3, "+1 txn on the base" ); + + $transactions = $ticket2->Transactions; + $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); + is( $transactions->Count, 3, "three txns on the target" ); +} +($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id, SilentTarget => 1); +ok($id,$msg); +is(link_count($filename), 0, "scrips ok"); + + +# restore +RT->Config->Set( LinkTransactionsRun1Scrip => $link_scrips_orig ); +RT->Config->Set( StrictLinkACL => $link_acl_checks_orig ); + +{ + my $Scrips = RT::Scrips->new( $RT::SystemUser ); + $Scrips->Limit( FIELD => 'Description', OPERATOR => 'STARTSWITH', VALUE => 'Add or Delete Link '); + while ( my $s = $Scrips->Next ) { $s->Delete }; +} + + +my $link = RT::Link->new( $RT::SystemUser ); +($id,$msg) = $link->Create( Base => $ticket->URI, Target => $ticket2->URI, Type => 'MyLinkType' ); +ok($id, $msg); +ok($link->LocalBase == $ticket->id, "LocalBase set correctly"); +ok($link->LocalTarget == $ticket2->id, "LocalTarget set correctly"); + +{ + no warnings 'once'; + *RT::NotTicket::Id = sub { return $$ }; + *RT::NotTicket::id = \&RT::NotTicket::Id; +} + +{ + package RT::URI::not_ticket; + use RT::URI::base; + use vars qw(@ISA); + @ISA = qw/RT::URI::base/; + sub IsLocal { 1; } + sub Object { return bless {}, 'RT::NotTicket'; } +} + +my $orig_getresolver = \&RT::URI::_GetResolver; +{ + no warnings 'redefine'; + *RT::URI::_GetResolver = sub { + my $self = shift; + my $scheme = shift; + + $scheme =~ s/(\.|-)/_/g; + my $resolver; + my $module = "RT::URI::$scheme"; + $resolver = $module->new($self->CurrentUser); + + if ($resolver) { + $self->{'resolver'} = $resolver; + } else { + $self->{'resolver'} = RT::URI::base->new($self->CurrentUser); + } + }; +} + +($id,$msg) = $link->Create( Base => "not_ticket::$RT::Organization/notticket/$$", Target => $ticket2->URI, Type => 'MyLinkType' ); +ok($id, $msg); +ok($link->LocalBase == 0, "LocalBase set correctly"); +ok($link->LocalTarget == $ticket2->id, "LocalTarget set correctly"); + +($id,$msg) = $link->Create( Target => "not_ticket::$RT::Organization/notticket/$$", Base => $ticket->URI, Type => 'MyLinkType' ); +ok($id, $msg); +ok($link->LocalTarget == 0, "LocalTarget set correctly"); +ok($link->LocalBase == $ticket->id, "LocalBase set correctly"); + +($id,$msg) = $link->Create( + Target => "not_ticket::$RT::Organization/notticket/1$$", + Base => "not_ticket::$RT::Organization/notticket/$$", + Type => 'MyLinkType' ); + +ok($id, $msg); +ok($link->LocalTarget == 0, "LocalTarget set correctly"); +ok($link->LocalBase == 0, "LocalBase set correctly"); + +# restore _GetResolver +{ + no warnings 'redefine'; + *RT::URI::_GetResolver = $orig_getresolver; +} + +sub link_count { + my $file = shift; + open my $fh, "<$file" or die "couldn't open $file"; + my $data = <$fh>; + close $fh; + + return undef unless $data; + chomp $data; + return $data + 0; +} diff --git a/rt/t/ticket/merge.t b/rt/t/ticket/merge.t new file mode 100644 index 000000000..a714cb6cc --- /dev/null +++ b/rt/t/ticket/merge.t @@ -0,0 +1,92 @@ +#!/usr/bin/perl + +use strict; +use warnings; + + +use RT; +use RT::Test tests => '17'; + + +# validate that when merging two tickets, the comments from both tickets +# are integrated into the new ticket +{ + my $queue = RT::Queue->new($RT::SystemUser); + my ($id,$msg) = $queue->Create(Name => 'MergeTest-'.rand(25)); + ok ($id,$msg); + + my $t1 = RT::Ticket->new($RT::SystemUser); + my ($tid,$transid, $t1msg) =$t1->Create ( Queue => $queue->Name, Subject => 'Merge test. orig'); + ok ($tid, $t1msg); + ($id, $msg) = $t1->Comment(Content => 'This is a Comment on the original'); + ok($id,$msg); + + my $txns = $t1->Transactions; + my $Comments = 0; + while (my $txn = $txns->Next) { + $Comments++ if ($txn->Type eq 'Comment'); + } + is($Comments,1, "our first ticket has only one Comment"); + + my $t2 = RT::Ticket->new($RT::SystemUser); + my ($t2id,$t2transid, $t2msg) =$t2->Create ( Queue => $queue->Name, Subject => 'Merge test. duplicate'); + ok ($t2id, $t2msg); + + + + ($id, $msg) = $t2->Comment(Content => 'This is a commet on the duplicate'); + ok($id,$msg); + + + $txns = $t2->Transactions; + $Comments = 0; + while (my $txn = $txns->Next) { + $Comments++ if ($txn->Type eq 'Comment'); + } + is($Comments,1, "our second ticket has only one Comment"); + + ($id, $msg) = $t1->Comment(Content => 'This is a second Comment on the original'); + ok($id,$msg); + + $txns = $t1->Transactions; + $Comments = 0; + while (my $txn = $txns->Next) { + $Comments++ if ($txn->Type eq 'Comment'); + } + is($Comments,2, "our first ticket now has two Comments"); + + ($id,$msg) = $t2->MergeInto($t1->id); + + ok($id,$msg); + $txns = $t1->Transactions; + $Comments = 0; + while (my $txn = $txns->Next) { + $Comments++ if ($txn->Type eq 'Comment'); + } + is($Comments,3, "our first ticket now has three Comments - we merged safely"); +} + +# when you try to merge duplicate links on postgres, eveyrything goes to hell due to referential integrity constraints. +{ + my $t = RT::Ticket->new($RT::SystemUser); + $t->Create(Subject => 'Main', Queue => 'general'); + + ok ($t->id); + my $t2 = RT::Ticket->new($RT::SystemUser); + $t2->Create(Subject => 'Second', Queue => 'general'); + ok ($t2->id); + + my $t3 = RT::Ticket->new($RT::SystemUser); + $t3->Create(Subject => 'Third', Queue => 'general'); + + ok ($t3->id); + + my ($id,$val); + ($id,$val) = $t->AddLink(Type => 'DependsOn', Target => $t3->id); + ok($id,$val); + ($id,$val) = $t2->AddLink(Type => 'DependsOn', Target => $t3->id); + ok($id,$val); + + ($id,$val) = $t->MergeInto($t2->id); + ok($id,$val); +} diff --git a/rt/t/ticket/quicksearch.t b/rt/t/ticket/quicksearch.t new file mode 100644 index 000000000..9ab9f21e4 --- /dev/null +++ b/rt/t/ticket/quicksearch.t @@ -0,0 +1,41 @@ + +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 10; +use_ok('RT'); + + +my $q = RT::Queue->new($RT::SystemUser); +my $queue = 'SearchTests-'.$$; +$q->Create(Name => $queue); +ok ($q->id, "Created the queue"); + +my $t1 = RT::Ticket->new($RT::SystemUser); +my ( $id, undef, $msg ) = $t1->Create( + Queue => $q->id, + Subject => 'SearchTest1', + Requestor => ['search2@example.com'], +); +ok( $id, $msg ); + +use_ok("RT::Search::Googleish"); + +my $active_statuses = join( " OR ", map "Status = '$_'", RT::Queue->ActiveStatusArray()); + +my $tickets = RT::Tickets->new($RT::SystemUser); +my $quick = RT::Search::Googleish->new(Argument => "", + TicketsObj => $tickets); +my @tests = ( + "General new open root" => "( Owner = 'root' ) AND ( Queue = 'General' ) AND ( Status = 'new' OR Status = 'open' )", + "fulltext:jesse" => "( Content LIKE 'jesse' ) AND ( $active_statuses )", + $queue => "( Queue = '$queue' ) AND ( $active_statuses )", + "root $queue" => "( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( $active_statuses )", + "notauser $queue" => "( Queue = '$queue' ) AND ( $active_statuses ) AND ( Subject LIKE 'notauser' )", + "notauser $queue root" => "( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( $active_statuses ) AND ( Subject LIKE 'notauser' )"); + +while (my ($from, $to) = splice @tests, 0, 2) { + is($quick->QueryToSQL($from), $to, "<$from> -> <$to>"); +} diff --git a/rt/t/ticket/requestor-order.t b/rt/t/ticket/requestor-order.t new file mode 100644 index 000000000..4539fbdc6 --- /dev/null +++ b/rt/t/ticket/requestor-order.t @@ -0,0 +1,142 @@ +#!/usr/bin/perl -w +use strict; use warnings; + +use RT::Test tests => 58; +use_ok('RT'); + +use RT::Ticket; + +my $q = RT::Queue->new($RT::SystemUser); +my $queue = 'SearchTests-'.rand(200); +$q->Create(Name => $queue); + +my @requestors = ( ('bravo@example.com') x 6, ('alpha@example.com') x 6, + ('delta@example.com') x 6, ('charlie@example.com') x 6, + (undef) x 6); +my @subjects = ("first test", "second test", "third test", "fourth test", "fifth test") x 6; +while (@requestors) { + my $t = RT::Ticket->new($RT::SystemUser); + my ( $id, undef $msg ) = $t->Create( + Queue => $q->id, + Subject => shift @subjects, + Requestor => [ shift @requestors ] + ); + ok( $id, $msg ); +} + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue'"); + is($tix->Count, 30, "found thirty tickets"); +} + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND requestor = 'alpha\@example.com'"); + $tix->OrderByCols({ FIELD => "Subject" }); + my @subjects; + while (my $t = $tix->Next) { push @subjects, $t->Subject; } + is(@subjects, 6, "found six tickets"); + is_deeply( \@subjects, [ sort @subjects ], "Subjects are sorted"); +} + +sub check_emails_order +{ + my ($tix,$count,$order) = (@_); + my @mails; + while (my $t = $tix->Next) { push @mails, $t->RequestorAddresses; } + is(@mails, $count, "found $count tickets for ". $tix->Query); + my @required_order; + if( $order =~ /asc/i ) { + @required_order = sort { $a? ($b? ($a cmp $b) : -1) : 1} @mails; + } else { + @required_order = sort { $a? ($b? ($b cmp $a) : -1) : 1} @mails; + } + foreach( reverse splice @mails ) { + if( $_ ) { unshift @mails, $_ } + else { push @mails, $_ } + } + is_deeply( \@mails, \@required_order, "Addresses are sorted"); +} + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND subject = 'first test' AND Requestor.EmailAddress LIKE 'example.com'"); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" }); + check_emails_order($tix, 5, 'ASC'); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' }); + check_emails_order($tix, 5, 'DESC'); +} + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Subject = 'first test'"); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" }); + check_emails_order($tix, 6, 'ASC'); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' }); + check_emails_order($tix, 6, 'DESC'); +} + + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Subject = 'first test'"); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" }); + check_emails_order($tix, 6, 'ASC'); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' }); + check_emails_order($tix, 6, 'DESC'); +} + +{ + # create ticket with group as member of the requestors group + my $t = RT::Ticket->new($RT::SystemUser); + my ( $id, $msg ) = $t->Create( + Queue => $q->id, + Subject => "first test", + Requestor => 'badaboom@example.com', + ); + ok( $id, "ticket created" ) or diag( "error: $msg" ); + + my $g = RT::Group->new($RT::SystemUser); + + my ($gid); + ($gid, $msg) = $g->CreateUserDefinedGroup(Name => '20-sort-by-requestor.t-'.rand(200)); + ok($gid, "created group") or diag("error: $msg"); + + ($id, $msg) = $t->Requestors->AddMember( $gid ); + ok($id, "added group to requestors group") or diag("error: $msg"); +} + + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Subject = 'first test'"); + + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" }); + check_emails_order($tix, 7, 'ASC'); + + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' }); + check_emails_order($tix, 7, 'DESC'); + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue'"); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" }); + $tix->RowsPerPage(30); + my @mails; + while (my $t = $tix->Next) { push @mails, $t->RequestorAddresses; } + is(@mails, 30, "found thirty tickets"); + is_deeply( [grep {$_} @mails], [ sort grep {$_} @mails ], "Paging works (exclude nulls, which are db-dependant)"); +} + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue'"); + $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" }); + $tix->RowsPerPage(30); + my @mails; + while (my $t = $tix->Next) { push @mails, $t->RequestorAddresses; } + is(@mails, 30, "found thirty tickets"); + is_deeply( [grep {$_} @mails], [ sort grep {$_} @mails ], "Paging works (exclude nulls, which are db-dependant)"); +} +RT::Test->mailsent_ok(25); + +# vim:ft=perl: diff --git a/rt/t/ticket/scrips_batch.t b/rt/t/ticket/scrips_batch.t new file mode 100644 index 000000000..f558d3bd4 --- /dev/null +++ b/rt/t/ticket/scrips_batch.t @@ -0,0 +1,100 @@ + +use strict; +use warnings; + +use RT::Test tests => '19'; +use_ok('RT'); +use_ok('RT::Ticket'); + +my $queue = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $queue && $queue->id, 'loaded or created queue'; + +RT->Config->Set( UseTransactionBatch => 1 ); + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in as root'; + +my $sid; +{ + $m->follow_link_ok( { text => 'Configuration' } ); + $m->follow_link_ok( { text => 'Queues' } ); + $m->follow_link_ok( { text => $queue->Name } ); + $m->follow_link_ok( { text => 'Scrips' } ); + $m->follow_link_ok( { text => 'New scrip' } ); + $m->form_number(3); + $m->field('Scrip-new-Description' => 'test'); + $m->select('Scrip-new-ScripCondition' => 'On Transaction'); + $m->select('Scrip-new-ScripAction' => 'User Defined'); + $m->select('Scrip-new-Template' => 'Global template: Blank'); + $m->select('Scrip-new-Stage' => 'TransactionBatch'); + $m->field('Scrip-new-CustomPrepareCode' => 'return 1;'); + $m->field('Scrip-new-CustomCommitCode' => 'return 1;'); + $m->submit; + $m->content_like( qr/Scrip Created/ ); + + ($sid) = ($m->content =~ /Scrip\s*#(\d+)/); + + my $form = $m->form_number(3); + is $m->value("Scrip-$sid-Description"), 'test', 'correct description'; + is value_name($form, "Scrip-$sid-ScripCondition"), 'On Transaction', 'correct condition'; + is value_name($form, "Scrip-$sid-ScripAction"), 'User Defined', 'correct action'; + is value_name($form, "Scrip-$sid-Template"), 'Global template: Blank', 'correct template'; + is value_name($form, "Scrip-$sid-Stage"), 'TransactionBatch', 'correct stage'; + + use File::Temp qw(tempfile); + my ($tmp_fh, $tmp_fn) = tempfile(); + + my $code = <<END; +open my \$fh, '>', '$tmp_fn' or die "Couldn't open '$tmp_fn':\$!"; + +my \$batch = \$self->TicketObj->TransactionBatch; +unless ( \$batch && \@\$batch ) { + print \$fh "no batch\n"; + return 1; +} +foreach my \$txn ( \@\$batch ) { + print \$fh \$txn->Type ."\n"; +} +return 1; +END + + $m->field( "Scrip-$sid-CustomCommitCode" => $code ); + $m->submit; + + $m->goto_create_ticket( $queue ); + $m->form_number(3); + $m->submit; + + is_deeply parse_handle($tmp_fh), ['Create'], 'Create'; + + $m->follow_link_ok( { text => 'Resolve' } ); + $m->form_number(3); + $m->field( "UpdateContent" => 'resolve it' ); + $m->click('SubmitTicket'); + + is_deeply parse_handle($tmp_fh), ['Comment', 'Status'], 'Comment + Resolve'; +} + +sub value_name { + my $form = shift; + my $field = shift; + + my $input = $form->find_input( $field ); + + my @names = $input->value_names; + my @values = $input->possible_values; + for ( my $i = 0; $i < @values; $i++ ) { + return $names[ $i ] if $values[ $i ] eq $input->value; + } + return undef; +} + +sub parse_handle { + my $fh = shift; + seek $fh, 0, 0; + my @lines = <$fh>; + foreach ( @lines ) { s/^\s+//gms; s/\s+$//gms } + truncate $fh, 0; + return \@lines; +} + diff --git a/rt/t/ticket/search.t b/rt/t/ticket/search.t new file mode 100644 index 000000000..9cec4f753 --- /dev/null +++ b/rt/t/ticket/search.t @@ -0,0 +1,278 @@ +#!/opt/perl/bin/perl -w + +# tests relating to searching. Especially around custom fields, and +# corner cases. + +use strict; +use warnings; + +use RT::Test tests => 43; + +# setup the queue + +my $q = RT::Queue->new($RT::SystemUser); +my $queue = 'SearchTests-'.$$; +$q->Create(Name => $queue); +ok ($q->id, "Created the queue"); + + +# and setup the CFs +# we believe the Type shouldn't matter. + +my $cf = RT::CustomField->new($RT::SystemUser); +$cf->Create(Name => 'SearchTest', Type => 'Freeform', MaxValues => 0, Queue => $q->id); +ok($cf->id, "Created the SearchTest CF"); +my $cflabel = "CustomField-".$cf->id; + +my $cf2 = RT::CustomField->new($RT::SystemUser); +$cf2->Create(Name => 'SearchTest2', Type => 'Freeform', MaxValues => 0, Queue => $q->id); +ok($cf2->id, "Created the SearchTest2 CF"); +my $cflabel2 = "CustomField-".$cf2->id; + +my $cf3 = RT::CustomField->new($RT::SystemUser); +$cf3->Create(Name => 'SearchTest3', Type => 'Freeform', MaxValues => 0, Queue => $q->id); +ok($cf3->id, "Created the SearchTest3 CF"); +my $cflabel3 = "CustomField-".$cf3->id; + + +# There was a bug involving a missing join to ObjectCustomFields that +# caused spurious results on negative searches if another custom field +# with the same name existed on a different queue. Hence, we make +# duplicate CFs on a different queue here +my $dup = RT::Queue->new($RT::SystemUser); +$dup->Create(Name => $queue . "-Copy"); +ok ($dup->id, "Created the duplicate queue"); +my $dupcf = RT::CustomField->new($RT::SystemUser); +$dupcf->Create(Name => 'SearchTest', Type => 'Freeform', MaxValues => 0, Queue => $dup->id); +ok($dupcf->id, "Created the duplicate SearchTest CF"); +$dupcf = RT::CustomField->new($RT::SystemUser); +$dupcf->Create(Name => 'SearchTest2', Type => 'Freeform', MaxValues => 0, Queue => $dup->id); +ok($dupcf->id, "Created the SearchTest2 CF"); +$dupcf = RT::CustomField->new($RT::SystemUser); +$dupcf->Create(Name => 'SearchTest3', Type => 'Freeform', MaxValues => 0, Queue => $dup->id); +ok($dupcf->id, "Created the SearchTest3 CF"); + + +# setup some tickets +# we'll need a small pile of them, to test various combinations and nulls. +# there's probably a way to think harder and do this with fewer + + +my $t1 = RT::Ticket->new($RT::SystemUser); +my ( $id, undef $msg ) = $t1->Create( + Queue => $q->id, + Subject => 'SearchTest1', + Requestor => ['search1@example.com'], + $cflabel => 'foo1', + $cflabel2 => 'bar1', + $cflabel3 => 'qux1', +); +ok( $id, $msg ); + + +my $t2 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t2->Create( + Queue => $q->id, + Subject => 'SearchTest2', + Requestor => ['search2@example.com'], +# $cflabel => 'foo2', + $cflabel2 => 'bar2', + $cflabel3 => 'qux2', +); +ok( $id, $msg ); + +my $t3 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t3->Create( + Queue => $q->id, + Subject => 'SearchTest3', + Requestor => ['search3@example.com'], + $cflabel => 'foo3', +# $cflabel2 => 'bar3', + $cflabel3 => 'qux3', +); +ok( $id, $msg ); + +my $t4 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t4->Create( + Queue => $q->id, + Subject => 'SearchTest4', + Requestor => ['search4@example.com'], + $cflabel => 'foo4', + $cflabel2 => 'bar4', +# $cflabel3 => 'qux4', +); +ok( $id, $msg ); + +my $t5 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t5->Create( + Queue => $q->id, +# Subject => 'SearchTest5', + Requestor => ['search5@example.com'], + $cflabel => 'foo5', + $cflabel2 => 'bar5', + $cflabel3 => 'qux5', +); +ok( $id, $msg ); + +my $t6 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t6->Create( + Queue => $q->id, + Subject => 'SearchTest6', +# Requestor => ['search6@example.com'], + $cflabel => 'foo6', + $cflabel2 => 'bar6', + $cflabel3 => 'qux6', +); +ok( $id, $msg ); + +my $t7 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t7->Create( + Queue => $q->id, + Subject => 'SearchTest7', + Requestor => ['search7@example.com'], +# $cflabel => 'foo7', +# $cflabel2 => 'bar7', + $cflabel3 => 'qux7', +); +ok( $id, $msg ); + +# we have tickets. start searching +my $tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue'"); +is($tix->Count, 7, "found all the tickets") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + + +# very simple searches. both CF and normal + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest = 'foo1'"); +is($tix->Count, 1, "matched identical subject") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo1'"); +is($tix->Count, 1, "matched LIKE subject") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest = 'foo'"); +is($tix->Count, 0, "IS a regexp match") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo'"); +is($tix->Count, 5, "matched LIKE subject") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest IS NULL"); +is($tix->Count, 2, "IS null CF") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Requestors LIKE 'search1'"); +is($tix->Count, 1, "LIKE requestor") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Requestors = 'search1\@example.com'"); +is($tix->Count, 1, "IS requestor") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Requestors LIKE 'search'"); +is($tix->Count, 6, "LIKE requestor") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Requestors IS NULL"); +is($tix->Count, 1, "Search for no requestor") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Subject = 'SearchTest1'"); +is($tix->Count, 1, "IS subject") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Subject LIKE 'SearchTest1'"); +is($tix->Count, 1, "LIKE subject") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Subject = ''"); +is($tix->Count, 1, "found one ticket") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Subject LIKE 'SearchTest'"); +is($tix->Count, 6, "found two ticket") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND Subject LIKE 'qwerty'"); +is($tix->Count, 0, "found zero ticket") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + + + + +# various combinations + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest LIKE 'foo' AND CF.SearchTest2 LIKE 'bar1'"); +is($tix->Count, 1, "LIKE cf and LIKE cf"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest = 'foo1' AND CF.SearchTest2 = 'bar1'"); +is($tix->Count, 1, "is cf and is cf"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest = 'foo' AND CF.SearchTest2 LIKE 'bar1'"); +is($tix->Count, 0, "is cf and like cf"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest LIKE 'foo' AND CF.SearchTest2 LIKE 'bar' AND CF.SearchTest3 LIKE 'qux'"); +is($tix->Count, 3, "like cf and like cf and like cf"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest LIKE 'foo' AND CF.SearchTest2 LIKE 'bar' AND CF.SearchTest3 LIKE 'qux6'"); +is($tix->Count, 1, "like cf and like cf and is cf"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest LIKE 'foo' AND Subject LIKE 'SearchTest'"); +is($tix->Count, 4, "like cf and like subject"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest IS NULL AND CF.SearchTest2 = 'bar2'"); +is($tix->Count, 1, "null cf and is cf"); + + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest IS NULL AND CF.SearchTest2 IS NULL"); +is($tix->Count, 1, "null cf and null cf"); + +# tests with the same CF listed twice + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.{SearchTest} = 'foo1'"); +is($tix->Count, 1, "is cf.{name} format"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest = 'foo1' OR CF.SearchTest = 'foo3'"); +is($tix->Count, 2, "is cf1 or is cf1"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest = 'foo1' OR CF.SearchTest IS NULL"); +is($tix->Count, 3, "is cf1 or null cf1"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("(CF.SearchTest = 'foo1' OR CF.SearchTest = 'foo3') AND (CF.SearchTest2 = 'bar1' OR CF.SearchTest2 = 'bar2')"); +is($tix->Count, 1, "(is cf1 or is cf1) and (is cf2 or is cf2)"); + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("CF.SearchTest = 'foo1' OR CF.SearchTest = 'foo3' OR CF.SearchTest2 = 'bar1' OR CF.SearchTest2 = 'bar2'"); +is($tix->Count, 3, "is cf1 or is cf1 or is cf2 or is cf2"); + diff --git a/rt/t/ticket/search_by_cf_freeform_multiple.t b/rt/t/ticket/search_by_cf_freeform_multiple.t new file mode 100644 index 000000000..be5130651 --- /dev/null +++ b/rt/t/ticket/search_by_cf_freeform_multiple.t @@ -0,0 +1,153 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 105; +use RT::Ticket; + +my $q = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $q && $q->id, 'loaded or created queue'; +my $queue = $q->Name; + +diag "create a CF\n" if $ENV{TEST_VERBOSE}; +my ($cf_name, $cf_id, $cf) = ("Test", 0, undef); +{ + $cf = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $cf->Create( + Name => $cf_name, + Queue => $q->id, + Type => 'FreeformMultiple', + ); + ok($ret, "Custom Field Order created"); + $cf_id = $cf->id; +} + +my ($total, @data, @tickets, %test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + while (@data) { + my %args = %{ shift(@data) }; + my @cf_value = $args{'Subject'} ne '-'? (split /(?=.)/, $args{'Subject'}) : (); + diag "vals: ". join ', ', @cf_value; + my $t = RT::Ticket->new($RT::SystemUser); + my ( $id, undef $msg ) = $t->Create( + Queue => $q->id, + %args, + "CustomField-$cf_id" => \@cf_value, + ); + ok( $id, "ticket created" ) or diag("error: $msg"); + + my $got = join ',', sort do { + my $vals = $t->CustomFieldValues( $cf_name ); + my @tmp; + while (my $v = $vals->Next ) { push @tmp, $v->Content } + @tmp; + }; + + is( $got, join( ',', sort @cf_value), 'correct CF values' ); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $key ( sort keys %test ) { + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL( "( $query_prefix ) AND ( $key )" ); + + my $error = 0; + + my $count = 0; + $count++ foreach grep $_, values %{ $test{$key} }; + is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1; + + my $good_tickets = ($tix->Count == $count); + while ( my $ticket = $tix->Next ) { + next if $test{$key}->{ $ticket->Subject }; + diag $ticket->Subject ." ticket has been found when it's not expected"; + $good_tickets = 0; + } + ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1; + + diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error; + } +} + +@data = ( + { Subject => '-' }, + { Subject => 'x' }, + { Subject => 'y' }, + { Subject => 'z' }, + { Subject => 'xy' }, + { Subject => 'xz' }, + { Subject => 'yz' }, +); +%test = ( + "CF.{$cf_id} IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + "'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + + "CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, + "'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, + "'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, + "'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, + + "CF.{$cf_id} = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.$queue.{$cf_id}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.$queue.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + + "CF.{$cf_id} != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 }, + "'CF.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 }, + "'CF.$queue.{$cf_id}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 }, + "'CF.$queue.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 }, + + "CF.{$cf_id} = 'x' OR CF.{$cf_id} = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 }, + "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 }, + "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 }, + "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 }, + + "CF.{$cf_id} = 'x' AND CF.{$cf_id} = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 }, + "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 }, + + "CF.{$cf_id} != 'x' AND CF.{$cf_id} != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 }, + "'CF.{$cf_name}' != 'x' AND 'CF.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_id}' != 'x' AND 'CF.$queue.{$cf_id}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_name}' != 'x' AND 'CF.$queue.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 }, + + "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 }, + + "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + + "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 }, + + "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, + "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, + "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, + "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 }, +); +@tickets = add_tix_from_data(); +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue'"); + is($tix->Count, $total, "found $total tickets"); +} +run_tests(); + +exit 0; diff --git a/rt/t/ticket/search_by_cf_freeform_single.t b/rt/t/ticket/search_by_cf_freeform_single.t new file mode 100644 index 000000000..d5ff7ec0d --- /dev/null +++ b/rt/t/ticket/search_by_cf_freeform_single.t @@ -0,0 +1,142 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 99; +use RT::Ticket; + +my $q = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $q && $q->id, 'loaded or created queue'; +my $queue = $q->Name; + +diag "create a CF\n" if $ENV{TEST_VERBOSE}; +my ($cf_name, $cf_id, $cf) = ("Test", 0, undef); +{ + $cf = RT::CustomField->new( $RT::SystemUser ); + my ($ret, $msg) = $cf->Create( + Name => $cf_name, + Queue => $q->id, + Type => 'FreeformSingle', + ); + ok($ret, "Custom Field Order created"); + $cf_id = $cf->id; +} + +my ($total, @data, @tickets, %test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + while (@data) { + my %args = %{ shift(@data) }; + my $cf_value = $args{'Subject'} ne '-'? $args{'Subject'} : undef; + my $t = RT::Ticket->new($RT::SystemUser); + my ( $id, undef $msg ) = $t->Create( + Queue => $q->id, + %args, + "CustomField-$cf_id" => $cf_value, + ); + ok( $id, "ticket created" ) or diag("error: $msg"); + is( $t->FirstCustomFieldValue( $cf_name ), $cf_value, 'correct value' ); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $key ( sort keys %test ) { + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL( "( $query_prefix ) AND ( $key )" ); + + my $error = 0; + + my $count = 0; + $count++ foreach grep $_, values %{ $test{$key} }; + is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1; + + my $good_tickets = ($tix->Count == $count); + while ( my $ticket = $tix->Next ) { + next if $test{$key}->{ $ticket->Subject }; + diag $ticket->Subject ." ticket has been found when it's not expected"; + $good_tickets = 0; + } + ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1; + + diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error; + } +} + +@data = ( + { Subject => '-' }, + { Subject => 'x' }, + { Subject => 'y' }, + { Subject => 'z' }, +); +%test = ( + "CF.{$cf_id} IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 }, + "'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 }, + "'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 }, + "'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 }, + + "CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + "'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + "'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + "'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + + "CF.{$cf_id} = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 }, + "'CF.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 }, + "'CF.$queue.{$cf_id}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 }, + "'CF.$queue.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 }, + + "CF.{$cf_id} != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 }, + "'CF.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 }, + "'CF.$queue.{$cf_id}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 }, + "'CF.$queue.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 }, + + "CF.{$cf_id} = 'x' OR CF.{$cf_id} = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 }, + "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 }, + "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 }, + "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 }, + + "CF.{$cf_id} = 'x' AND CF.{$cf_id} = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 }, + "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 }, + "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 }, + "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 }, + + "CF.{$cf_id} != 'x' AND CF.{$cf_id} != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 }, + "'CF.{$cf_name}' != 'x' AND 'CF.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 }, + "'CF.$queue.{$cf_id}' != 'x' AND 'CF.$queue.{$cf_id}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 }, + "'CF.$queue.{$cf_name}' != 'x' AND 'CF.$queue.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 }, + + "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 }, + "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 }, + "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 }, + "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 }, + + "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 }, + "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 }, + "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 }, + "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 }, + + "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 }, + "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 }, + "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 }, + "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 }, + + "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 }, + +); +@tickets = add_tix_from_data(); +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue'"); + is($tix->Count, $total, "found $total tickets"); +} +run_tests(); + +exit 0; diff --git a/rt/t/ticket/search_by_links.t b/rt/t/ticket/search_by_links.t new file mode 100644 index 000000000..a8e955c8b --- /dev/null +++ b/rt/t/ticket/search_by_links.t @@ -0,0 +1,132 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 80; +use RT::Ticket; + +my $q = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $q && $q->id, 'loaded or created queue'; + +my ($total, @data, @tickets, %test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + while (@data) { + my $t = RT::Ticket->new($RT::SystemUser); + my %args = %{ shift(@data) }; + $args{$_} = $res[ $args{$_} ]->id foreach grep $args{$_}, keys %RT::Ticket::LINKTYPEMAP; + my ( $id, undef $msg ) = $t->Create( + Queue => $q->id, + %args, + ); + ok( $id, "ticket created" ) or diag("error: $msg"); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $key ( sort keys %test ) { + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL( "( $query_prefix ) AND ( $key )" ); + + my $error = 0; + + my $count = 0; + $count++ foreach grep $_, values %{ $test{$key} }; + is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1; + + my $good_tickets = 1; + while ( my $ticket = $tix->Next ) { + next if $test{$key}->{ $ticket->Subject }; + diag $ticket->Subject ." ticket has been found when it's not expected"; + $good_tickets = 0; + } + ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1; + + diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error; + } +} + +# simple set with "no links", "parent and child" +@data = ( + { Subject => '-', }, + { Subject => 'p', }, + { Subject => 'c', MemberOf => -1 }, +); +@tickets = add_tix_from_data(); +%test = ( + 'Linked IS NOT NULL' => { '-' => 0, c => 1, p => 1 }, + 'Linked IS NULL' => { '-' => 1, c => 0, p => 0 }, + 'LinkedTo IS NOT NULL' => { '-' => 0, c => 1, p => 0 }, + 'LinkedTo IS NULL' => { '-' => 1, c => 0, p => 1 }, + 'LinkedFrom IS NOT NULL' => { '-' => 0, c => 0, p => 1 }, + 'LinkedFrom IS NULL' => { '-' => 1, c => 1, p => 0 }, + + 'HasMember IS NOT NULL' => { '-' => 0, c => 0, p => 1 }, + 'HasMember IS NULL' => { '-' => 1, c => 1, p => 0 }, + 'MemberOf IS NOT NULL' => { '-' => 0, c => 1, p => 0 }, + 'MemberOf IS NULL' => { '-' => 1, c => 0, p => 1 }, + + 'RefersTo IS NOT NULL' => { '-' => 0, c => 0, p => 0 }, + 'RefersTo IS NULL' => { '-' => 1, c => 1, p => 1 }, + + 'Linked = '. $tickets[0]->id => { '-' => 0, c => 0, p => 0 }, + 'Linked != '. $tickets[0]->id => { '-' => 1, c => 1, p => 1 }, + + 'MemberOf = '. $tickets[1]->id => { '-' => 0, c => 1, p => 0 }, + 'MemberOf != '. $tickets[1]->id => { '-' => 1, c => 0, p => 1 }, +); +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '". $q->id ."'"); + is($tix->Count, $total, "found $total tickets"); +} +run_tests(); + +# another set with tests of combinations searches +@data = ( + { Subject => '-', }, + { Subject => 'p', }, + { Subject => 'rp', RefersTo => -1 }, + { Subject => 'c', MemberOf => -2 }, + { Subject => 'rc1', RefersTo => -1 }, + { Subject => 'rc2', RefersTo => -2 }, +); +@tickets = add_tix_from_data(); +my $pid = $tickets[1]->id; +%test = ( + 'RefersTo IS NOT NULL' => { '-' => 0, c => 0, p => 0, rp => 1, rc1 => 1, rc2 => 1 }, + 'RefersTo IS NULL' => { '-' => 1, c => 1, p => 1, rp => 0, rc1 => 0, rc2 => 0 }, + + 'RefersTo IS NOT NULL AND MemberOf IS NOT NULL' => { '-' => 0, c => 0, p => 0, rp => 0, rc1 => 0, rc2 => 0 }, + 'RefersTo IS NOT NULL AND MemberOf IS NULL' => { '-' => 0, c => 0, p => 0, rp => 1, rc1 => 1, rc2 => 1 }, + 'RefersTo IS NULL AND MemberOf IS NOT NULL' => { '-' => 0, c => 1, p => 0, rp => 0, rc1 => 0, rc2 => 0 }, + 'RefersTo IS NULL AND MemberOf IS NULL' => { '-' => 1, c => 0, p => 1, rp => 0, rc1 => 0, rc2 => 0 }, + + 'RefersTo IS NOT NULL OR MemberOf IS NOT NULL' => { '-' => 0, c => 1, p => 0, rp => 1, rc1 => 1, rc2 => 1 }, + 'RefersTo IS NOT NULL OR MemberOf IS NULL' => { '-' => 1, c => 0, p => 1, rp => 1, rc1 => 1, rc2 => 1 }, + 'RefersTo IS NULL OR MemberOf IS NOT NULL' => { '-' => 1, c => 1, p => 1, rp => 0, rc1 => 0, rc2 => 0 }, + 'RefersTo IS NULL OR MemberOf IS NULL' => { '-' => 1, c => 1, p => 1, rp => 1, rc1 => 1, rc2 => 1 }, + + "RefersTo = $pid AND MemberOf = $pid" => { '-' => 0, c => 0, p => 0, rp => 0, rc1 => 0, rc2 => 0 }, + "RefersTo = $pid AND MemberOf != $pid" => { '-' => 0, c => 0, p => 0, rp => 1, rc1 => 0, rc2 => 0 }, + "RefersTo != $pid AND MemberOf = $pid" => { '-' => 0, c => 1, p => 0, rp => 0, rc1 => 0, rc2 => 0 }, + "RefersTo != $pid AND MemberOf != $pid" => { '-' => 1, c => 0, p => 1, rp => 0, rc1 => 1, rc2 => 1 }, + + "RefersTo = $pid OR MemberOf = $pid" => { '-' => 0, c => 1, p => 0, rp => 1, rc1 => 0, rc2 => 0 }, + "RefersTo = $pid OR MemberOf != $pid" => { '-' => 1, c => 0, p => 1, rp => 1, rc1 => 1, rc2 => 1 }, + "RefersTo != $pid OR MemberOf = $pid" => { '-' => 1, c => 1, p => 1, rp => 0, rc1 => 1, rc2 => 1 }, + "RefersTo != $pid OR MemberOf != $pid" => { '-' => 1, c => 1, p => 1, rp => 1, rc1 => 1, rc2 => 1 }, +); +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '". $q->id ."'"); + is($tix->Count, $total, "found $total tickets"); +} +run_tests(); + diff --git a/rt/t/ticket/search_by_txn.t b/rt/t/ticket/search_by_txn.t new file mode 100644 index 000000000..1be6916ef --- /dev/null +++ b/rt/t/ticket/search_by_txn.t @@ -0,0 +1,35 @@ +#!/usr/bin/perl + +use warnings; +use strict; + + +BEGIN{ $ENV{'TZ'} = 'GMT'}; + +use RT::Test tests => 10; + +my $SUBJECT = "Search test - ".$$; + +use_ok('RT::Tickets'); +my $tix = RT::Tickets->new($RT::SystemUser); +can_ok($tix, 'FromSQL'); +$tix->FromSQL('Updated = "2005-08-05" AND Subject = "$SUBJECT"'); + +ok(! $tix->Count, "Searching for tickets updated on a random date finds nothing" . $tix->Count); + +my $ticket = RT::Ticket->new($RT::SystemUser); +$ticket->Create(Queue => 'General', Subject => $SUBJECT); +ok ($ticket->id, "We created a ticket"); +my ($id, $txnid, $txnobj) = $ticket->Comment( Content => 'A comment that happend on 2004-01-01'); + +isa_ok($txnobj, 'RT::Transaction'); + +ok($txnobj->CreatedObj->ISO); +my ( $sid,$smsg) = $txnobj->__Set(Field => 'Created', Value => '2005-08-05 20:00:56'); +ok($sid,$smsg); +is($txnobj->Created,'2005-08-05 20:00:56'); +is($txnobj->CreatedObj->ISO,'2005-08-05 20:00:56'); + +$tix->FromSQL(qq{Updated = "2005-08-05" AND Subject = "$SUBJECT"}); +is( $tix->Count, 1); + diff --git a/rt/t/ticket/search_by_watcher.t b/rt/t/ticket/search_by_watcher.t new file mode 100644 index 000000000..9d94432d2 --- /dev/null +++ b/rt/t/ticket/search_by_watcher.t @@ -0,0 +1,280 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 119; +use RT::Ticket; + +my $q = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $q && $q->id, 'loaded or created queue'; +my $queue = $q->Name; + +my ($total, @data, @tickets, %test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + while (@data) { + my $t = RT::Ticket->new($RT::SystemUser); + my ( $id, undef $msg ) = $t->Create( + Queue => $q->id, + %{ shift(@data) }, + ); + ok( $id, "ticket created" ) or diag("error: $msg"); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $key ( sort keys %test ) { + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL( "( $query_prefix ) AND ( $key )" ); + + my $error = 0; + + my $count = 0; + $count++ foreach grep $_, values %{ $test{$key} }; + is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1; + + my $good_tickets = ($tix->Count == $count); + while ( my $ticket = $tix->Next ) { + next if $test{$key}->{ $ticket->Subject }; + diag $ticket->Subject ." ticket has been found when it's not expected"; + $good_tickets = 0; + } + ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1; + + diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error; + } +} + +@data = ( + { Subject => 'xy', Requestor => ['x@example.com', 'y@example.com'] }, + { Subject => 'x', Requestor => 'x@example.com' }, + { Subject => 'y', Requestor => 'y@example.com' }, + { Subject => '-', }, + { Subject => 'z', Requestor => 'z@example.com' }, +); +%test = ( + 'Requestor = "x@example.com"' => { xy => 1, x => 1, y => 0, '-' => 0, z => 0 }, + 'Requestor != "x@example.com"' => { xy => 0, x => 0, y => 1, '-' => 1, z => 1 }, + + 'Requestor = "y@example.com"' => { xy => 1, x => 0, y => 1, '-' => 0, z => 0 }, + 'Requestor != "y@example.com"' => { xy => 0, x => 1, y => 0, '-' => 1, z => 1 }, + + 'Requestor LIKE "@example.com"' => { xy => 1, x => 1, y => 1, '-' => 0, z => 1 }, + 'Requestor NOT LIKE "@example.com"' => { xy => 0, x => 0, y => 0, '-' => 1, z => 0 }, + + 'Requestor IS NULL' => { xy => 0, x => 0, y => 0, '-' => 1, z => 0 }, + 'Requestor IS NOT NULL' => { xy => 1, x => 1, y => 1, '-' => 0, z => 1 }, + +# this test is a todo, we run it later +# 'Requestor = "x@example.com" AND Requestor = "y@example.com"' => { xy => 1, x => 0, y => 0, '-' => 0, z => 0 }, + 'Requestor = "x@example.com" OR Requestor = "y@example.com"' => { xy => 1, x => 1, y => 1, '-' => 0, z => 0 }, + + 'Requestor != "x@example.com" AND Requestor != "y@example.com"' => { xy => 0, x => 0, y => 0, '-' => 1, z => 1 }, + 'Requestor != "x@example.com" OR Requestor != "y@example.com"' => { xy => 0, x => 1, y => 1, '-' => 1, z => 1 }, + + 'Requestor = "x@example.com" AND Requestor != "y@example.com"' => { xy => 0, x => 1, y => 0, '-' => 0, z => 0 }, + 'Requestor = "x@example.com" OR Requestor != "y@example.com"' => { xy => 1, x => 1, y => 0, '-' => 1, z => 1 }, + + 'Requestor != "x@example.com" AND Requestor = "y@example.com"' => { xy => 0, x => 0, y => 1, '-' => 0, z => 0 }, + 'Requestor != "x@example.com" OR Requestor = "y@example.com"' => { xy => 1, x => 0, y => 1, '-' => 1, z => 1 }, +); +@tickets = add_tix_from_data(); +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue'"); + is($tix->Count, $total, "found $total tickets"); +} +run_tests(); + +# mixing searches by watchers with other conditions +# http://rt3.fsck.com/Ticket/Display.html?id=9322 +%test = ( + 'Subject LIKE "x" AND Requestor = "y@example.com"' => + { xy => 1, x => 0, y => 0, '-' => 0, z => 0 }, + 'Subject NOT LIKE "x" AND Requestor = "y@example.com"' => + { xy => 0, x => 0, y => 1, '-' => 0, z => 0 }, + 'Subject LIKE "x" AND Requestor != "y@example.com"' => + { xy => 0, x => 1, y => 0, '-' => 0, z => 0 }, + 'Subject NOT LIKE "x" AND Requestor != "y@example.com"' => + { xy => 0, x => 0, y => 0, '-' => 1, z => 1 }, + + 'Subject LIKE "x" OR Requestor = "y@example.com"' => + { xy => 1, x => 1, y => 1, '-' => 0, z => 0 }, + 'Subject NOT LIKE "x" OR Requestor = "y@example.com"' => + { xy => 1, x => 0, y => 1, '-' => 1, z => 1 }, + 'Subject LIKE "x" OR Requestor != "y@example.com"' => + { xy => 1, x => 1, y => 0, '-' => 1, z => 1 }, + 'Subject NOT LIKE "x" OR Requestor != "y@example.com"' => + { xy => 0, x => 1, y => 1, '-' => 1, z => 1 }, + +# group of cases when user doesn't exist in DB at all + 'Subject LIKE "x" AND Requestor = "not-exist@example.com"' => + { xy => 0, x => 0, y => 0, '-' => 0, z => 0 }, + 'Subject NOT LIKE "x" AND Requestor = "not-exist@example.com"' => + { xy => 0, x => 0, y => 0, '-' => 0, z => 0 }, + 'Subject LIKE "x" AND Requestor != "not-exist@example.com"' => + { xy => 1, x => 1, y => 0, '-' => 0, z => 0 }, + 'Subject NOT LIKE "x" AND Requestor != "not-exist@example.com"' => + { xy => 0, x => 0, y => 1, '-' => 1, z => 1 }, +# 'Subject LIKE "x" OR Requestor = "not-exist@example.com"' => +# { xy => 1, x => 1, y => 0, '-' => 0, z => 0 }, +# 'Subject NOT LIKE "x" OR Requestor = "not-exist@example.com"' => +# { xy => 0, x => 0, y => 1, '-' => 1, z => 1 }, + 'Subject LIKE "x" OR Requestor != "not-exist@example.com"' => + { xy => 1, x => 1, y => 1, '-' => 1, z => 1 }, + 'Subject NOT LIKE "x" OR Requestor != "not-exist@example.com"' => + { xy => 1, x => 1, y => 1, '-' => 1, z => 1 }, + + 'Subject LIKE "z" AND (Requestor = "x@example.com" OR Requestor = "y@example.com")' => + { xy => 0, x => 0, y => 0, '-' => 0, z => 0 }, + 'Subject NOT LIKE "z" AND (Requestor = "x@example.com" OR Requestor = "y@example.com")' => + { xy => 1, x => 1, y => 1, '-' => 0, z => 0 }, + 'Subject LIKE "z" OR (Requestor = "x@example.com" OR Requestor = "y@example.com")' => + { xy => 1, x => 1, y => 1, '-' => 0, z => 1 }, + 'Subject NOT LIKE "z" OR (Requestor = "x@example.com" OR Requestor = "y@example.com")' => + { xy => 1, x => 1, y => 1, '-' => 1, z => 0 }, +); +run_tests(); + +TODO: { + local $TODO = "we can't generate this query yet"; + %test = ( + 'Requestor = "x@example.com" AND Requestor = "y@example.com"' + => { xy => 1, x => 0, y => 0, '-' => 0, z => 0 }, + 'Subject LIKE "x" OR Requestor = "not-exist@example.com"' => + { xy => 1, x => 1, y => 0, '-' => 0, z => 0 }, + 'Subject NOT LIKE "x" OR Requestor = "not-exist@example.com"' => + { xy => 0, x => 0, y => 1, '-' => 1, z => 1 }, + ); + run_tests(); +} + +@data = ( + { Subject => 'xy', Cc => ['x@example.com'], Requestor => [ 'y@example.com' ] }, + { Subject => 'x-', Cc => ['x@example.com'], Requestor => [] }, + { Subject => '-y', Cc => [], Requestor => [ 'y@example.com' ] }, + { Subject => '-', }, + { Subject => 'zz', Cc => ['z@example.com'], Requestor => [ 'z@example.com' ] }, + { Subject => 'z-', Cc => ['z@example.com'], Requestor => [] }, + { Subject => '-z', Cc => [], Requestor => [ 'z@example.com' ] }, +); +%test = ( + 'Cc = "x@example.com" AND Requestor = "y@example.com"' => + { xy => 1, 'x-' => 0, '-y' => 0, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 }, + 'Cc = "x@example.com" OR Requestor = "y@example.com"' => + { xy => 1, 'x-' => 1, '-y' => 1, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 }, + + 'Cc != "x@example.com" AND Requestor = "y@example.com"' => + { xy => 0, 'x-' => 0, '-y' => 1, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 }, + 'Cc != "x@example.com" OR Requestor = "y@example.com"' => + { xy => 1, 'x-' => 0, '-y' => 1, '-' => 1, zz => 1, 'z-' => 1, '-z' => 1 }, + + 'Cc IS NULL AND Requestor = "y@example.com"' => + { xy => 0, 'x-' => 0, '-y' => 1, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 }, + 'Cc IS NULL OR Requestor = "y@example.com"' => + { xy => 1, 'x-' => 0, '-y' => 1, '-' => 1, zz => 0, 'z-' => 0, '-z' => 1 }, + + 'Cc IS NOT NULL AND Requestor = "y@example.com"' => + { xy => 1, 'x-' => 0, '-y' => 0, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 }, + 'Cc IS NOT NULL OR Requestor = "y@example.com"' => + { xy => 1, 'x-' => 1, '-y' => 1, '-' => 0, zz => 1, 'z-' => 1, '-z' => 0 }, +); +@tickets = add_tix_from_data(); +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue'"); + is($tix->Count, $total, "found $total tickets"); +} +run_tests(); + + +# owner is special watcher because reference is duplicated in two places, +# owner was an ENUM field now it's WATCHERFIELD, but should support old +# style ENUM searches for backward compatibility +my $nobody = RT::Nobody(); +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Owner = '". $nobody->id ."'"); + ok($tix->Count, "found ticket(s)"); +} +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Owner = '". $nobody->Name ."'"); + ok($tix->Count, "found ticket(s)"); +} +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Owner != '". $nobody->id ."'"); + is($tix->Count, 0, "found ticket(s)"); +} +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Owner != '". $nobody->Name ."'"); + is($tix->Count, 0, "found ticket(s)"); +} + +{ + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Owner.Name LIKE 'nob'"); + ok($tix->Count, "found ticket(s)"); +} + +{ + # create ticket and force type to not a 'ticket' value + # bug #6898@rt3.fsck.com + # and http://marc.theaimsgroup.com/?l=rt-devel&m=112662934627236&w=2 + @data = ( { Subject => 'not a ticket' } ); + my($t) = add_tix_from_data(); + $t->_Set( Field => 'Type', + Value => 'not a ticket', + CheckACL => 0, + RecordTransaction => 0, + ); + $total--; + + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND Owner = 'Nobody'"); + is($tix->Count, $total, "found ticket(s)"); +} + +{ + my $everyone = RT::Group->new( $RT::SystemUser ); + $everyone->LoadSystemInternalGroup('Everyone'); + ok($everyone->id, "loaded 'everyone' group"); + my($id, $msg) = $everyone->PrincipalObj->GrantRight( Right => 'OwnTicket', + Object => $q + ); + ok($id, "granted OwnTicket right to Everyone on '$queue'") or diag("error: $msg"); + + my $u = RT::User->new( $RT::SystemUser ); + $u->LoadOrCreateByEmail('alpha@example.com'); + ok($u->id, "loaded user"); + @data = ( { Subject => '4', Owner => $u->id } ); + my($t) = add_tix_from_data(); + is( $t->Owner, $u->id, "created ticket with custom owner" ); + my $u_alpha_id = $u->id; + + $u = RT::User->new( $RT::SystemUser ); + $u->LoadOrCreateByEmail('bravo@example.com'); + ok($u->id, "loaded user"); + @data = ( { Subject => '5', Owner => $u->id } ); + ($t) = add_tix_from_data(); + is( $t->Owner, $u->id, "created ticket with custom owner" ); + my $u_bravo_id = $u->id; + + my $tix = RT::Tickets->new($RT::SystemUser); + $tix->FromSQL("Queue = '$queue' AND + ( Owner = '$u_alpha_id' OR + Owner = '$u_bravo_id' )" + ); + is($tix->Count, 2, "found ticket(s)"); +} + + +exit(0) diff --git a/rt/t/ticket/search_long_cf_values.t b/rt/t/ticket/search_long_cf_values.t new file mode 100644 index 000000000..f9cc7b5a2 --- /dev/null +++ b/rt/t/ticket/search_long_cf_values.t @@ -0,0 +1,79 @@ +#!/opt/perl/bin/perl -w + +# tests relating to searching. Especially around custom fields with long values +# (> 255 chars) + +use strict; +use warnings; + +use RT::Test tests => 10; + +# setup the queue + +my $q = RT::Queue->new($RT::SystemUser); +my $queue = 'SearchTests-'.$$; +$q->Create(Name => $queue); +ok ($q->id, "Created the queue"); + + +# setup the CF +my $cf = RT::CustomField->new($RT::SystemUser); +$cf->Create(Name => 'SearchTest', Type => 'Freeform', MaxValues => 0, Queue => $q->id); +ok($cf->id, "Created the SearchTest CF"); +my $cflabel = "CustomField-".$cf->id; + +# setup some tickets +my $t1 = RT::Ticket->new($RT::SystemUser); +my ( $id, undef $msg ) = $t1->Create( + Queue => $q->id, + Subject => 'SearchTest1', + Requestor => ['search@example.com'], + $cflabel => 'foo', +); +ok( $id, $msg ); + + +my $t2 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t2->Create( + Queue => $q->id, + Subject => 'SearchTest2', + Requestor => ['searchlong@example.com'], + $cflabel => 'bar' x 150, +); +ok( $id, $msg ); + +my $t3 = RT::Ticket->new($RT::SystemUser); +( $id, undef, $msg ) = $t3->Create( + Queue => $q->id, + Subject => 'SearchTest3', + Requestor => ['searchlong@example.com'], + $cflabel => 'bar', +); +ok( $id, $msg ); + +# we have tickets. start searching +my $tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo'"); +is($tix->Count, 1, "matched short string foo") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'bar'"); +is($tix->Count, 2, "matched long+short string bar") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND ( CF.SearchTest LIKE 'foo' OR CF.SearchTest LIKE 'bar' )"); +is($tix->Count, 3, "matched short string foo or long+short string bar") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest NOT LIKE 'foo' AND CF.SearchTest LIKE 'bar'"); +is($tix->Count, 2, "not matched short string foo and matched long+short string bar") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + +$tix = RT::Tickets->new($RT::SystemUser); +$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo' AND CF.SearchTest NOT LIKE 'bar'"); +is($tix->Count, 1, "matched short string foo and not matched long+short string bar") + or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery; + diff --git a/rt/t/ticket/sort-by-custom-ownership.t b/rt/t/ticket/sort-by-custom-ownership.t new file mode 100644 index 000000000..9739c5aec --- /dev/null +++ b/rt/t/ticket/sort-by-custom-ownership.t @@ -0,0 +1,103 @@ +#!/usr/bin/perl + +use RT; +use RT::Test tests => 7; + + +use strict; +use warnings; + +use RT::Tickets; +use RT::Queue; +use RT::CustomField; + +my($ret,$msg); + +# Test Paw Sort + + + +# ---- Create a queue to test with. +my $queue = "PAWSortQueue-$$"; +my $queue_obj = RT::Queue->new($RT::SystemUser); +($ret, $msg) = $queue_obj->Create(Name => $queue, + Description => 'queue for custom field sort testing'); +ok($ret, "$queue test queue creation. $msg"); + + +# ---- Create some users + +my $me = RT::User->new($RT::SystemUser); +($ret, $msg) = $me->Create(Name => "Me$$", EmailAddress => $$.'create-me-1@example.com'); +($ret, $msg) = $me->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'OwnTicket'); +($ret, $msg) = $me->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'SeeQueue'); +($ret, $msg) = $me->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'ShowTicket'); +my $you = RT::User->new($RT::SystemUser); +($ret, $msg) = $you->Create(Name => "You$$", EmailAddress => $$.'create-you-1@example.com'); +($ret, $msg) = $you->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'OwnTicket'); +($ret, $msg) = $you->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'SeeQueue'); +($ret, $msg) = $you->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'ShowTicket'); + +my $nobody = RT::User->new($RT::SystemUser); +$nobody->Load('nobody'); + + +# ----- Create some tickets to test with. Assign them some values to +# make it easy to sort with. + +my @tickets = ( + [qw[1 10], $me], + [qw[2 20], $me], + [qw[3 20], $you], + [qw[4 30], $you], + [qw[5 5], $nobody], + [qw[6 55], $nobody], + ); +for (@tickets) { + my $t = RT::Ticket->new($RT::SystemUser); + $t->Create( Queue => $queue_obj->Id, + Subject => $_->[0], + Owner => $_->[2]->Id, + Priority => $_->[1], + ); +} + +sub check_order { + my ($tx, @order) = @_; + my @results; + while (my $t = $tx->Next) { + push @results, $t->Subject; + } + my $results = join (" ",@results); + my $order = join(" ",@order); + is( $results, $order ); +} + + +# The real tests start here + +my $cme = new RT::CurrentUser( $me ); +my $metx = new RT::Tickets( $cme ); +# Make sure we can sort in both directions on a queue specific field. +$metx->FromSQL(qq[queue="$queue"] ); +$metx->OrderBy( FIELD => "Custom.Ownership", ORDER => 'ASC' ); +is($metx->Count,6); +check_order( $metx, qw[2 1 6 5 4 3]); + +$metx->OrderBy( FIELD => "Custom.Ownership", ORDER => 'DESC' ); +is($metx->Count,6); +check_order( $metx, reverse qw[2 1 6 5 4 3]); + + + +my $cyou = new RT::CurrentUser( $you ); +my $youtx = new RT::Tickets( $cyou ); +# Make sure we can sort in both directions on a queue specific field. +$youtx->FromSQL(qq[queue="$queue"] ); +$youtx->OrderBy( FIELD => "Custom.Ownership", ORDER => 'ASC' ); +is($youtx->Count,6); +check_order( $youtx, qw[4 3 6 5 2 1]); + +__END__ + + diff --git a/rt/t/ticket/sort-by-queue.t b/rt/t/ticket/sort-by-queue.t new file mode 100644 index 000000000..df6e1ad0f --- /dev/null +++ b/rt/t/ticket/sort-by-queue.t @@ -0,0 +1,100 @@ +#!/usr/bin/perl + +use RT::Test tests => 8; + +use strict; +use warnings; + +use RT::Tickets; +use RT::Queue; +use RT::CustomField; + +######################################################### +# Test sorting by Queue, we sort by its name +######################################################### + + +diag "Create queues to test with." if $ENV{TEST_VERBOSE}; +my @qids; +my @queues; +# create them in reverse order to avoid false positives +foreach my $name ( qw(sort-by-queue-Z sort-by-queue-A) ) { + my $queue = RT::Queue->new( $RT::SystemUser ); + my ($ret, $msg) = $queue->Create( + Name => $name ."-$$", + Description => 'queue to test sorting by queue' + ); + ok($ret, "test queue creation. $msg"); + push @queues, $queue; + push @qids, $queue->id; +} + +my ($total, @data, @tickets, @test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + @data = sort { rand(100) <=> rand(100) } @data; + while (@data) { + my $t = RT::Ticket->new($RT::SystemUser); + my %args = %{ shift(@data) }; + my ( $id, undef, $msg ) = $t->Create( %args ); + ok( $id, "ticket created" ) or diag("error: $msg"); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $test ( @test ) { + my $query = join " AND ", map "( $_ )", grep defined && length, + $query_prefix, $test->{'Query'}; + + foreach my $order (qw(ASC DESC)) { + my $error = 0; + my $tix = RT::Tickets->new( $RT::SystemUser ); + $tix->FromSQL( $query ); + $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order ); + + ok($tix->Count, "found ticket(s)") + or $error = 1; + + my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz'); + while ( my $t = $tix->Next ) { + my $tmp; + if ( $order eq 'ASC' ) { + $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]); + } else { + $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]); + } + if ( $tmp > 0 ) { + $order_ok = 0; last; + } + $last = $t->Subject; + } + + ok( $order_ok, "$order order of tickets is good" ) + or $error = 1; + + if ( $error ) { + diag "Wrong SQL query:". $tix->BuildSelectQuery; + $tix->GotoFirstItem; + while ( my $t = $tix->Next ) { + diag sprintf "%02d - %s", $t->id, $t->Subject; + } + } + } + } +} + +@data = ( + { Queue => $qids[0], Subject => 'z' }, + { Queue => $qids[1], Subject => 'a' }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "Queue" }, +); +run_tests(); + diff --git a/rt/t/ticket/sort-by-user.t b/rt/t/ticket/sort-by-user.t new file mode 100644 index 000000000..f9b1602f1 --- /dev/null +++ b/rt/t/ticket/sort-by-user.t @@ -0,0 +1,152 @@ +#!/usr/bin/perl + +use RT::Test tests => 32; + +use strict; +use warnings; + +use RT::Tickets; +use RT::Queue; +use RT::CustomField; + +######################################################### +# Test sorting by Owner, Creator and LastUpdatedBy +# we sort by user name +######################################################### + +diag "Create a queue to test with." if $ENV{TEST_VERBOSE}; +my $queue_name = "OwnerSortQueue$$"; +my $queue; +{ + $queue = RT::Queue->new( $RT::SystemUser ); + my ($ret, $msg) = $queue->Create( + Name => $queue_name, + Description => 'queue for custom field sort testing' + ); + ok($ret, "$queue test queue creation. $msg"); +} + +my @uids; +my @users; +# create them in reverse order to avoid false positives +foreach my $u (qw(Z A)) { + my $name = $u ."-user-to-test-ordering-$$"; + my $user = RT::User->new( $RT::SystemUser ); + my ($uid) = $user->Create( + Name => $name, + Privileged => 1, + ); + ok $uid, "created user #$uid"; + + my ($status, $msg) = $user->PrincipalObj->GrantRight( Right => 'OwnTicket', Object => $queue ); + ok $status, "granted right"; + ($status, $msg) = $user->PrincipalObj->GrantRight( Right => 'CreateTicket', Object => $queue ); + ok $status, "granted right"; + + push @users, $user; + push @uids, $user->id; +} + +my ($total, @data, @tickets, @test) = (0, ()); + +sub add_tix_from_data { + my @res = (); + @data = sort { rand(100) <=> rand(100) } @data; + while (@data) { + my $t = RT::Ticket->new($RT::SystemUser); + my %args = %{ shift(@data) }; + + my ( $id, undef, $msg ) = $t->Create( %args, Queue => $queue->id ); + if ( $args{'Owner'} ) { + is $t->Owner, $args{'Owner'}, "owner is correct"; + } + if ( $args{'Creator'} ) { + is $t->Creator, $args{'Creator'}, "creator is correct"; + } + # hackish, but simpler + if ( $args{'LastUpdatedBy'} ) { + $t->__Set( Field => 'LastUpdatedBy', Value => $args{'LastUpdatedBy'} ); + } + ok( $id, "ticket created" ) or diag("error: $msg"); + push @res, $t; + $total++; + } + return @res; +} + +sub run_tests { + my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets; + foreach my $test ( @test ) { + my $query = join " AND ", map "( $_ )", grep defined && length, + $query_prefix, $test->{'Query'}; + + foreach my $order (qw(ASC DESC)) { + my $error = 0; + my $tix = RT::Tickets->new( $RT::SystemUser ); + $tix->FromSQL( $query ); + $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order ); + + ok($tix->Count, "found ticket(s)") + or $error = 1; + + my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz'); + while ( my $t = $tix->Next ) { + my $tmp; + if ( $order eq 'ASC' ) { + $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]); + } else { + $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]); + } + if ( $tmp > 0 ) { + $order_ok = 0; last; + } + $last = $t->Subject; + } + + ok( $order_ok, "$order order of tickets is good" ) + or $error = 1; + + if ( $error ) { + diag "Wrong SQL query:". $tix->BuildSelectQuery; + $tix->GotoFirstItem; + while ( my $t = $tix->Next ) { + diag sprintf "%02d - %s", $t->id, $t->Subject; + } + } + } + } +} + +@data = ( + { Subject => 'Nobody' }, + { Subject => 'Z', Owner => $uids[0] }, + { Subject => 'A', Owner => $uids[1] }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "Owner" }, +); +run_tests(); + +@data = ( + { Subject => 'RT' }, + { Subject => 'Z', Creator => $uids[0] }, + { Subject => 'A', Creator => $uids[1] }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "Creator" }, +); +run_tests(); + +@data = ( + { Subject => 'RT' }, + { Subject => 'Z', LastUpdatedBy => $uids[0] }, + { Subject => 'A', LastUpdatedBy => $uids[1] }, +); +@tickets = add_tix_from_data(); +@test = ( + { Order => "LastUpdatedBy" }, +); +run_tests(); + diff --git a/rt/t/ticket/sort_by_cf.t b/rt/t/ticket/sort_by_cf.t new file mode 100644 index 000000000..69274add9 --- /dev/null +++ b/rt/t/ticket/sort_by_cf.t @@ -0,0 +1,172 @@ +#!/usr/bin/perl + +use RT::Test tests => 21; +RT::Init(); + +use strict; +use warnings; + +use RT::Tickets; +use RT::Queue; +use RT::CustomField; + +my($ret,$msg); + + +# Test Sorting by custom fields. +# TODO: it's hard to read this file, conver to new style, +# for example look at 23cfsort-freeform-single.t + +# ---- Create a queue to test with. +my $queue = "CFSortQueue-$$"; +my $queue_obj = RT::Queue->new( $RT::SystemUser ); +($ret, $msg) = $queue_obj->Create( + Name => $queue, + Description => 'queue for custom field sort testing' +); +ok($ret, "$queue test queue creation. $msg"); + +# ---- Create some custom fields. We're not currently using all of +# them to test with, but the more the merrier. +my $cfO = RT::CustomField->new($RT::SystemUser); +my $cfA = RT::CustomField->new($RT::SystemUser); +my $cfB = RT::CustomField->new($RT::SystemUser); +my $cfC = RT::CustomField->new($RT::SystemUser); + +($ret, $msg) = $cfO->Create( Name => 'Order', + Queue => 0, + SortOrder => 1, + Description => q{Something to compare results for, since we can't guarantee ticket ID}, + Type=> 'FreeformSingle'); +ok($ret, "Custom Field Order created"); + +($ret, $msg) = $cfA->Create( Name => 'Alpha', + Queue => $queue_obj->id, + SortOrder => 1, + Description => 'A Testing custom field', + Type=> 'FreeformSingle'); +ok($ret, "Custom Field Alpha created"); + +($ret, $msg) = $cfB->Create( Name => 'Beta', + Queue => $queue_obj->id, + Description => 'A Testing custom field', + Type=> 'FreeformSingle'); +ok($ret, "Custom Field Beta created"); + +($ret, $msg) = $cfC->Create( Name => 'Charlie', + Queue => $queue_obj->id, + Description => 'A Testing custom field', + Type=> 'FreeformSingle'); +ok($ret, "Custom Field Charlie created"); + +# ----- Create some tickets to test with. Assign them some values to +# make it easy to sort with. +my $t1 = RT::Ticket->new($RT::SystemUser); +$t1->Create( Queue => $queue_obj->Id, + Subject => 'One', + ); +$t1->AddCustomFieldValue(Field => $cfO->Id, Value => '1'); +$t1->AddCustomFieldValue(Field => $cfA->Id, Value => '2'); +$t1->AddCustomFieldValue(Field => $cfB->Id, Value => '1'); +$t1->AddCustomFieldValue(Field => $cfC->Id, Value => 'BBB'); + +my $t2 = RT::Ticket->new($RT::SystemUser); +$t2->Create( Queue => $queue_obj->Id, + Subject => 'Two', + ); +$t2->AddCustomFieldValue(Field => $cfO->Id, Value => '2'); +$t2->AddCustomFieldValue(Field => $cfA->Id, Value => '1'); +$t2->AddCustomFieldValue(Field => $cfB->Id, Value => '2'); +$t2->AddCustomFieldValue(Field => $cfC->Id, Value => 'AAA'); + +# helper +sub check_order { + my ($tx, @order) = @_; + my @results; + while (my $t = $tx->Next) { + push @results, $t->CustomFieldValues($cfO->Id)->First->Content; + } + my $results = join (" ",@results); + my $order = join(" ",@order); + @_ = ($results, $order , "Ordered correctly: $order"); + goto \&is; +} + +# The real tests start here +my $tx = new RT::Tickets( $RT::SystemUser ); + + +# Make sure we can sort in both directions on a queue specific field. +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderBy( FIELD => "CF.${queue}.{Charlie}", ORDER => 'DES' ); +is($tx->Count,2 ,"We found 2 tickets when looking for cf charlie"); +check_order( $tx, 1, 2); + +$tx = new RT::Tickets( $RT::SystemUser ); +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderBy( FIELD => "CF.${queue}.{Charlie}", ORDER => 'ASC' ); +is($tx->Count,2, "We found two tickets when sorting by cf charlie without limiting to it" ); +check_order( $tx, 2, 1); + +# When ordering by _global_ CustomFields, if more than one queue has a +# CF named Charlie, things will go bad. So, these results are uniqued +# in Tickets_Overlay. +$tx = new RT::Tickets( $RT::SystemUser ); +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderBy( FIELD => "CF.{Charlie}", ORDER => 'DESC' ); +is($tx->Count,2); +check_order( $tx, 1, 2); + +$tx = new RT::Tickets( $RT::SystemUser ); +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderBy( FIELD => "CF.{Charlie}", ORDER => 'ASC' ); +is($tx->Count,2); +check_order( $tx, 2, 1); + +# Add a new ticket, to test sorting on multiple columns. +my $t3 = RT::Ticket->new($RT::SystemUser); +$t3->Create( Queue => $queue_obj->Id, + Subject => 'Three', + ); +$t3->AddCustomFieldValue(Field => $cfO->Id, Value => '3'); +$t3->AddCustomFieldValue(Field => $cfA->Id, Value => '3'); +$t3->AddCustomFieldValue(Field => $cfB->Id, Value => '2'); +$t3->AddCustomFieldValue(Field => $cfC->Id, Value => 'AAA'); + +$tx = new RT::Tickets( $RT::SystemUser ); +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderByCols( + { FIELD => "CF.${queue}.{Charlie}", ORDER => 'ASC' }, + { FIELD => "CF.${queue}.{Alpha}", ORDER => 'DES' }, +); +is($tx->Count,3); +check_order( $tx, 3, 2, 1); + +$tx = new RT::Tickets( $RT::SystemUser ); +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderByCols( + { FIELD => "CF.${queue}.{Charlie}", ORDER => 'DES' }, + { FIELD => "CF.${queue}.{Alpha}", ORDER => 'ASC' }, +); +is($tx->Count,3); +check_order( $tx, 1, 2, 3); + +# Reverse the order of the secondary column, which changes the order +# of the first two tickets. +$tx = new RT::Tickets( $RT::SystemUser ); +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderByCols( + { FIELD => "CF.${queue}.{Charlie}", ORDER => 'ASC' }, + { FIELD => "CF.${queue}.{Alpha}", ORDER => 'ASC' }, +); +is($tx->Count,3); +check_order( $tx, 2, 3, 1); + +$tx = new RT::Tickets( $RT::SystemUser ); +$tx->FromSQL(qq[queue="$queue"] ); +$tx->OrderByCols( + { FIELD => "CF.${queue}.{Charlie}", ORDER => 'DES' }, + { FIELD => "CF.${queue}.{Alpha}", ORDER => 'DES' }, +); +is($tx->Count,3); +check_order( $tx, 1, 3, 2); diff --git a/rt/t/validator/group_members.t b/rt/t/validator/group_members.t new file mode 100644 index 000000000..f27a1f177 --- /dev/null +++ b/rt/t/validator/group_members.t @@ -0,0 +1,178 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use RT::Test tests => 60; + +sub load_or_create_group { + my $name = shift; + my %args = (@_); + + my $group = RT::Group->new( $RT::SystemUser ); + $group->LoadUserDefinedGroup( $name ); + unless ( $group->id ) { + my ($id, $msg) = $group->CreateUserDefinedGroup( + Name => $name, + ); + die "$msg" unless $id; + } + + if ( $args{Members} ) { + my $cur = $group->MembersObj; + while ( my $entry = $cur->Next ) { + my ($status, $msg) = $entry->Delete; + die "$msg" unless $status; + } + + foreach my $new ( @{ $args{Members} } ) { + my ($status, $msg) = $group->AddMember( + ref($new)? $new->id : $new, + ); + die "$msg" unless $status; + } + } + + return $group; +} + +my $validator_path = "$RT::SbinPath/rt-validator"; +sub run_validator { + my %args = (check => 1, resolve => 0, force => 1, @_ ); + + my $cmd = $validator_path; + die "Couldn't find $cmd command" unless -f $cmd; + + while( my ($k,$v) = each %args ) { + next unless $v; + $cmd .= " --$k '$v'"; + } + $cmd .= ' 2>&1'; + + require IPC::Open2; + my ($child_out, $child_in); + my $pid = IPC::Open2::open2($child_out, $child_in, $cmd); + close $child_in; + + my $result = do { local $/; <$child_out> }; + close $child_out; + waitpid $pid, 0; + + DBIx::SearchBuilder::Record::Cachable->FlushCache + if $args{'resolve'}; + + return ($?, $result); +} + +{ + my ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; +} + +{ + my $group = load_or_create_group('test', Members => [] ); + ok $group, "loaded or created a group"; + + my ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; +} + +# G1 -> G2 +{ + my $group1 = load_or_create_group( 'test1', Members => [] ); + ok $group1, "loaded or created a group"; + + my $group2 = load_or_create_group( 'test2', Members => [ $group1 ]); + ok $group2, "loaded or created a group"; + + ok $group2->HasMember( $group1->id ), "has member"; + ok $group2->HasMemberRecursively( $group1->id ), "has member"; + + my ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; + + $RT::Handle->dbh->do("DELETE FROM CachedGroupMembers"); + DBIx::SearchBuilder::Record::Cachable->FlushCache; + ok !$group2->HasMemberRecursively( $group1->id ), "has no member, broken DB"; + + ($ecode, $res) = run_validator(resolve => 1); + + ok $group2->HasMember( $group1->id ), "has member"; + ok $group2->HasMemberRecursively( $group1->id ), "has member"; + + ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; +} + +# G1 <- G2 <- G3 <- G4 <- G5 +{ + my @groups; + for (1..5) { + my $child = @groups? $groups[-1]: undef; + + my $group = load_or_create_group( 'test'. $_, Members => [ $child? ($child): () ] ); + ok $group, "loaded or created a group"; + + ok $group->HasMember( $child->id ), "has member" + if $child; + ok $group->HasMemberRecursively( $_->id ), "has member" + foreach @groups; + + push @groups, $group; + } + + my ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; + + $RT::Handle->dbh->do("DELETE FROM CachedGroupMembers"); + DBIx::SearchBuilder::Record::Cachable->FlushCache; + + ok !$groups[1]->HasMemberRecursively( $groups[0]->id ), "has no member, broken DB"; + + ($ecode, $res) = run_validator(resolve => 1); + + for ( my $i = 1; $i < @groups; $i++ ) { + ok $groups[$i]->HasMember( $groups[$i-1]->id ), "has member"; + ok $groups[$i]->HasMemberRecursively( $groups[$_]->id ), "has member" + foreach 0..$i-1; + } + + ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; +} + +# G1 <- (G2, G3, G4, G5) +{ + my @groups; + for (2..5) { + my $group = load_or_create_group( 'test'. $_, Members => [] ); + ok $group, "loaded or created a group"; + push @groups, $group; + } + + my $parent = load_or_create_group( 'test1', Members => \@groups ); + ok $parent, "loaded or created a group"; + + my ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; +} + +# G1 <- (G2, G3, G4) <- G5 +{ + my $gchild = load_or_create_group( 'test5', Members => [] ); + ok $gchild, "loaded or created a group"; + + my @groups; + for (2..4) { + my $group = load_or_create_group( 'test'. $_, Members => [ $gchild ] ); + ok $group, "loaded or created a group"; + push @groups, $group; + } + + my $parent = load_or_create_group( 'test1', Members => \@groups ); + ok $parent, "loaded or created a group"; + + my ($ecode, $res) = run_validator(); + is $res, '', 'empty result'; +} + diff --git a/rt/t/web/attachments.t b/rt/t/web/attachments.t new file mode 100644 index 000000000..e827b2f02 --- /dev/null +++ b/rt/t/web/attachments.t @@ -0,0 +1,47 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 14; + +use constant LogoFile => $RT::MasonComponentRoot .'/NoAuth/images/bplogo.gif'; +use constant FaviconFile => $RT::MasonComponentRoot .'/NoAuth/images/favicon.png'; + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + +my $queue = RT::Queue->new($RT::Nobody); +my $qid = $queue->Load('General'); +ok( $qid, "Loaded General queue" ); + +$m->form_name('CreateTicketInQueue'); +$m->field('Queue', $qid); +$m->submit; +is($m->status, 200, "request successful"); +$m->content_like(qr/Create a new ticket/, 'ticket create page'); + +$m->form_name('TicketCreate'); +$m->field('Subject', 'Attachments test'); +$m->field('Attach', LogoFile); +$m->field('Content', 'Some content'); +$m->submit; +is($m->status, 200, "request successful"); + +$m->content_like(qr/Attachments test/, 'we have subject on the page'); +$m->content_like(qr/Some content/, 'and content'); +$m->content_like(qr/Download bplogo\.gif/, 'page has file name'); + +$m->follow_link_ok({text => 'Reply'}, "reply to the ticket"); +$m->form_name('TicketUpdate'); +$m->field('Attach', LogoFile); +$m->click('AddMoreAttach'); +is($m->status, 200, "request successful"); + +$m->form_name('TicketUpdate'); +$m->field('Attach', FaviconFile); +$m->field('UpdateContent', 'Message'); +$m->click('SubmitTicket'); +is($m->status, 200, "request successful"); + +$m->content_like(qr/Download bplogo\.gif/, 'page has file name'); +$m->content_like(qr/Download favicon\.png/, 'page has file name'); + diff --git a/rt/t/web/basic.t b/rt/t/web/basic.t new file mode 100644 index 000000000..bc4d65587 --- /dev/null +++ b/rt/t/web/basic.t @@ -0,0 +1,146 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Encode; + +use RT::Test tests => 24; +$RT::Test::SKIP_REQUEST_WORK_AROUND = 1; + +my ($baseurl, $agent) = RT::Test->started_ok; + +my $url = $agent->rt_base_url; +diag $url if $ENV{TEST_VERBOSE}; + +# get the top page +{ + $agent->get($url); + is ($agent->{'status'}, 200, "Loaded a page"); +} + +# test a login +{ + ok($agent->{form}->find_input('user')); + ok($agent->{form}->find_input('pass')); + + ok($agent->{'content'} =~ /username:/i); + $agent->field( 'user' => 'root' ); + $agent->field( 'pass' => 'password' ); + + # the field isn't named, so we have to click link 0 + $agent->click(0); + is( $agent->{'status'}, 200, "Fetched the page ok"); + ok( $agent->{'content'} =~ /Logout/i, "Found a logout link"); +} + +{ + $agent->get($url."Ticket/Create.html?Queue=1"); + is ($agent->{'status'}, 200, "Loaded Create.html"); + $agent->form_number(3); + my $string = Encode::decode_utf8("I18N Web Testing æøå"); + $agent->field('Subject' => "Ticket with utf8 body"); + $agent->field('Content' => $string); + ok($agent->submit, "Created new ticket with $string as Content"); + $agent->content_like( qr{$string} , "Found the content"); + ok($agent->{redirected_uri}, "Did redirection"); + + { + my $ticket = RT::Test->last_ticket; + my $content = $ticket->Transactions->First->Content; + like( + $content, qr{$string}, + 'content is there, API check' + ); + } +} + +{ + $agent->get($url."Ticket/Create.html?Queue=1"); + is ($agent->{'status'}, 200, "Loaded Create.html"); + $agent->form_number(3); + + my $string = Encode::decode_utf8("I18N Web Testing æøå"); + $agent->field('Subject' => $string); + $agent->field('Content' => "Ticket with utf8 subject"); + ok($agent->submit, "Created new ticket with $string as Content"); + $agent->content_like( qr{$string} , "Found the content"); + ok($agent->{redirected_uri}, "Did redirection"); + + { + my $ticket = RT::Test->last_ticket; + is( + $ticket->Subject, $string, + 'subject is correct, API check' + ); + } +} + +# Update time worked in hours +{ + $agent->follow_link( text_regex => qr/Basics/ ); + $agent->submit_form( form_number => 3, + fields => { TimeWorked => 5, 'TimeWorked-TimeUnits' => "hours" } + ); + + like ($agent->{'content'}, qr/to '300'/, "5 hours is 300 minutes"); +} + +# {{{ test an image + +TODO: { + todo_skip("Need to handle mason trying to compile images",1); +$agent->get( $url."NoAuth/images/test.png" ); +my $file = RT::Test::get_relocatable_file( + File::Spec->catfile( + qw(.. .. share html NoAuth images test.png) + ) +); +is( + length($agent->content), + -s $file, + "got a file of the correct size ($file)", +); +} +# }}} + +# {{{ Query Builder tests +# +# XXX: hey-ho, we have these tests in t/web/query-builder +# TODO: move everything about QB there + +my $response = $agent->get($url."Search/Build.html"); +ok( $response->is_success, "Fetched " . $url."Search/Build.html" ); + +# Parsing TicketSQL +# +# Adding items + +# set the first value +ok($agent->form_name('BuildQuery')); +$agent->field("AttachmentField", "Subject"); +$agent->field("AttachmentOp", "LIKE"); +$agent->field("ValueOfAttachment", "aaa"); +$agent->submit("AddClause"); + +# set the next value +ok($agent->form_name('BuildQuery')); +$agent->field("AttachmentField", "Subject"); +$agent->field("AttachmentOp", "LIKE"); +$agent->field("ValueOfAttachment", "bbb"); +$agent->submit("AddClause"); + +ok($agent->form_name('BuildQuery')); + +# get the query +my $query = $agent->current_form->find_input("Query")->value; +# strip whitespace from ends +$query =~ s/^\s*//g; +$query =~ s/\s*$//g; + +# collapse other whitespace +$query =~ s/\s+/ /g; + +is ($query, "Subject LIKE 'aaa' AND Subject LIKE 'bbb'"); + + +1; diff --git a/rt/t/web/cf_access.t b/rt/t/web/cf_access.t new file mode 100644 index 000000000..1022c6da6 --- /dev/null +++ b/rt/t/web/cf_access.t @@ -0,0 +1,191 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 26; +$RT::Test::SKIP_REQUEST_WORK_AROUND = 1; + +my ($baseurl, $m) = RT::Test->started_ok; + +use constant ImageFile => $RT::MasonComponentRoot .'/NoAuth/images/bplogo.gif'; +use constant ImageFileContent => RT::Test->file_content(ImageFile); + +ok $m->login, 'logged in'; + +diag "Create a CF" if $ENV{'TEST_VERBOSE'}; +{ + $m->follow_link( text => 'Configuration' ); + $m->title_is(q/RT Administration/, 'admin screen'); + $m->follow_link( text => 'Custom Fields' ); + $m->title_is(q/Select a Custom Field/, 'admin-cf screen'); + $m->follow_link( text => 'Create' ); + $m->submit_form( + form_name => "ModifyCustomField", + fields => { + TypeComposite => 'Image-0', + LookupType => 'RT::Queue-RT::Ticket', + Name => 'img', + Description => 'img', + }, + ); +} + +diag "apply the CF to General queue" if $ENV{'TEST_VERBOSE'}; +my ( $cf, $cfid, $tid ); +{ + $m->title_is(q/Created CustomField img/, 'admin-cf created'); + $m->follow_link( text => 'Queues' ); + $m->title_is(q/Admin queues/, 'admin-queues screen'); + $m->follow_link( text => 'General' ); + $m->title_is(q/Editing Configuration for queue General/, 'admin-queue: general'); + $m->follow_link( text => 'Ticket Custom Fields' ); + + $m->title_is(q/Edit Custom Fields for General/, 'admin-queue: general cfid'); + $m->form_name('EditCustomFields'); + + # Sort by numeric IDs in names + my @names = map { $_->[1] } + sort { $a->[0] <=> $b->[0] } + map { /Object-1-CF-(\d+)/ ? [ $1 => $_ ] : () } + grep defined, map $_->name, $m->current_form->inputs; + $cf = pop(@names); + $cf =~ /(\d+)$/ or die "Hey this is impossible dude"; + $cfid = $1; + $m->field( $cf => 1 ); # Associate the new CF with this queue + $m->field( $_ => undef ) for @names; # ...and not any other. ;-) + $m->submit; + + $m->content_like( qr/Object created/, 'TCF added to the queue' ); +} + +my $tester = RT::Test->load_or_create_user( Name => 'tester', Password => '123456' ); +RT::Test->set_rights( + { Principal => $tester->PrincipalObj, + Right => [qw(SeeQueue ShowTicket CreateTicket)], + }, +); +ok $m->login( $tester->Name, 123456), 'logged in'; + +diag "check that we have no the CF on the create" + ." ticket page when user has no SeeCustomField right" + if $ENV{'TEST_VERBOSE'}; +{ + $m->submit_form( + form_name => "CreateTicketInQueue", + fields => { Queue => 'General' }, + ); + $m->content_unlike(qr/Upload multiple images/, 'has no upload image field'); + + my $form = $m->form_name("TicketCreate"); + my $upload_field = "Object-RT::Ticket--CustomField-$cfid-Upload"; + ok !$form->find_input( $upload_field ), 'no form field on the page'; + + $m->submit_form( + form_name => "TicketCreate", + fields => { Subject => 'test' }, + ); + $m->content_like(qr/Ticket \d+ created/, "a ticket is created succesfully"); + + $m->content_unlike(qr/img:/, 'has no img field on the page'); + $m->follow_link( text => 'Custom Fields'); + $m->content_unlike(qr/Upload multiple images/, 'has no upload image field'); +} + +RT::Test->set_rights( + { Principal => $tester->PrincipalObj, + Right => [qw(SeeQueue ShowTicket CreateTicket SeeCustomField)], + }, +); + +diag "check that we have no the CF on the create" + ." ticket page when user has no ModifyCustomField right" + if $ENV{'TEST_VERBOSE'}; +{ + $m->submit_form( + form_name => "CreateTicketInQueue", + fields => { Queue => 'General' }, + ); + $m->content_unlike(qr/Upload multiple images/, 'has no upload image field'); + + my $form = $m->form_name("TicketCreate"); + my $upload_field = "Object-RT::Ticket--CustomField-$cfid-Upload"; + ok !$form->find_input( $upload_field ), 'no form field on the page'; + + $m->submit_form( + form_name => "TicketCreate", + fields => { Subject => 'test' }, + ); + $tid = $1 if $m->content =~ /Ticket (\d+) created/i; + ok $tid, "a ticket is created succesfully"; + + $m->follow_link( text => 'Custom Fields' ); + $m->content_unlike(qr/Upload multiple images/, 'has no upload image field'); + $form = $m->form_number(3); + $upload_field = "Object-RT::Ticket-$tid-CustomField-$cfid-Upload"; + ok !$form->find_input( $upload_field ), 'no form field on the page'; +} + +RT::Test->set_rights( + { Principal => $tester->PrincipalObj, + Right => [qw(SeeQueue ShowTicket CreateTicket SeeCustomField ModifyCustomField)], + }, +); + +diag "create a ticket with an image" if $ENV{'TEST_VERBOSE'}; +{ + $m->submit_form( + form_name => "CreateTicketInQueue", + fields => { Queue => 'General' }, + ); + $m->content_like(qr/Upload multiple images/, 'has a upload image field'); + + $cf =~ /(\d+)$/ or die "Hey this is impossible dude"; + my $upload_field = "Object-RT::Ticket--CustomField-$1-Upload"; + + $m->submit_form( + form_name => "TicketCreate", + fields => { + $upload_field => ImageFile, + Subject => 'testing img cf creation', + }, + ); + + $m->content_like(qr/Ticket \d+ created/, "a ticket is created succesfully"); + + $tid = $1 if $m->content =~ /Ticket (\d+) created/; + + $m->title_like(qr/testing img cf creation/, "its title is the Subject"); + + $m->follow_link( text => 'bplogo.gif' ); + $m->content_is(ImageFileContent, "it links to the uploaded image"); +} + +$m->get( $m->rt_base_url ); +$m->follow_link( text => 'Tickets' ); +$m->follow_link( text => 'New Query' ); + +$m->title_is(q/Query Builder/, 'Query building'); +$m->submit_form( + form_name => "BuildQuery", + fields => { + idOp => '=', + ValueOfid => $tid, + ValueOfQueue => 'General', + }, + button => 'AddClause', +); + +$m->form_name('BuildQuery'); + +my $col = ($m->current_form->find_input('SelectDisplayColumns'))[-1]; +$col->value( ($col->possible_values)[-1] ); + +$m->click('AddCol'); + +$m->form_name('BuildQuery'); +$m->click('DoSearch'); + +$m->follow_link( text_regex => qr/bplogo\.gif/ ); +$m->content_is(ImageFileContent, "it links to the uploaded image"); + +__END__ +[FC] Bulk Update does not have custom fields. diff --git a/rt/t/web/cf_onqueue.t b/rt/t/web/cf_onqueue.t new file mode 100644 index 000000000..dcd585277 --- /dev/null +++ b/rt/t/web/cf_onqueue.t @@ -0,0 +1,66 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 14; +my ($baseurl, $m) = RT::Test->started_ok; + +ok $m->login, 'logged in'; + +diag "Create a queue CF" if $ENV{'TEST_VERBOSE'}; +{ + $m->follow_link( text => 'Configuration' ); + $m->title_is(q/RT Administration/, 'admin screen'); + $m->follow_link( text => 'Custom Fields' ); + $m->title_is(q/Select a Custom Field/, 'admin-cf screen'); + $m->follow_link( text => 'Create' ); + $m->submit_form( + form_name => "ModifyCustomField", + fields => { + TypeComposite => 'Freeform-1', + LookupType => 'RT::Queue', + Name => 'QueueCFTest', + Description => 'QueueCFTest', + }, + ); + $m->content_like( qr/Object created/, 'CF QueueCFTest created' ); +} + +diag "Apply the new CF globally" if $ENV{'TEST_VERBOSE'}; +{ + $m->follow_link( text => 'Global' ); + $m->title_is(q!Admin/Global configuration!, 'global configuration screen'); + $m->follow_link( url_regex => qr!Admin/Global/CustomFields/index! ); + $m->title_is(q/Global custom field configuration/, 'global custom field configuration screen'); + $m->follow_link( url => 'Queues.html' ); + $m->title_is(q/Edit Custom Fields for all queues/, 'global custom field for all queues configuration screen'); + $m->content_like( qr/QueueCFTest/, 'CF QueueCFTest displayed on page' ); + $m->submit_form( + form_name => "EditCustomFields", + fields => { + 'Object--CF-1' => '1', + }, + ); + $m->content_like( qr/Object created/, 'CF QueueCFTest enabled globally' ); +} + +diag "Edit the CF value for default queue" if $ENV{'TEST_VERBOSE'}; +{ + $m->follow_link( url => '/Admin/Queues/' ); + $m->title_is(q/Admin queues/, 'queues configuration screen'); + $m->follow_link( text => "1" ); + $m->title_is(q/Editing Configuration for queue General/, 'default queue configuration screen'); + $m->content_like( qr/QueueCFTest/, 'CF QueueCFTest displayed on default queue' ); + $m->submit_form( + form_number => 3, + # The following doesn't want to works :( + #with_fields => { 'Object-RT::Queue-1-CustomField-1-Value' }, + fields => { + 'Object-RT::Queue-1-CustomField-1-Value' => 'QueueCFTest content', + }, + ); + $m->content_like( qr/QueueCFTest QueueCFTest content added/, 'Content filed in CF QueueCFTest for default queue' ); + +} + + +__END__ diff --git a/rt/t/web/cf_select_one.t b/rt/t/web/cf_select_one.t new file mode 100644 index 000000000..e009af7cf --- /dev/null +++ b/rt/t/web/cf_select_one.t @@ -0,0 +1,159 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use RT::Test tests => 41; + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in as root'; + +my $cf_name = 'test select one value'; + +my $cfid; +diag "Create a CF" if $ENV{'TEST_VERBOSE'}; +{ + $m->follow_link( text => 'Configuration' ); + $m->title_is(q/RT Administration/, 'admin screen'); + $m->follow_link( text => 'Custom Fields' ); + $m->title_is(q/Select a Custom Field/, 'admin-cf screen'); + $m->follow_link( text => 'Create' ); + $m->submit_form( + form_name => "ModifyCustomField", + fields => { + Name => $cf_name, + TypeComposite => 'Select-1', + LookupType => 'RT::Queue-RT::Ticket', + }, + ); + $m->content_like( qr/Object created/, 'created CF sucessfully' ); + $cfid = $m->form_name('ModifyCustomField')->value('id'); + ok $cfid, "found id of the CF in the form, it's #$cfid"; +} + +diag "add 'qwe', 'ASD' and '0' as values to the CF" if $ENV{'TEST_VERBOSE'}; +{ + foreach my $value(qw(qwe ASD 0)) { + $m->submit_form( + form_name => "ModifyCustomField", + fields => { + "CustomField-". $cfid ."-Value-new-Name" => $value, + }, + button => 'Update', + ); + $m->content_like( qr/Object created/, 'added a value to the CF' ); # or diag $m->content; + } +} + +my $queue = RT::Test->load_or_create_queue( Name => 'General' ); +ok $queue && $queue->id, 'loaded or created queue'; + +diag "apply the CF to General queue" if $ENV{'TEST_VERBOSE'}; +{ + $m->follow_link( text => 'Queues' ); + $m->title_is(q/Admin queues/, 'admin-queues screen'); + $m->follow_link( text => 'General' ); + $m->title_is(q/Editing Configuration for queue General/, 'admin-queue: general'); + $m->follow_link( text => 'Ticket Custom Fields' ); + $m->title_is(q/Edit Custom Fields for General/, 'admin-queue: general cfid'); + + $m->form_name('EditCustomFields'); + $m->field( "Object-". $queue->id ."-CF-$cfid" => 1 ); + $m->submit; + + $m->content_like( qr/Object created/, 'TCF added to the queue' ); +} + +my $tid; +diag "create a ticket using API with 'asd'(not 'ASD') as value of the CF" + if $ENV{'TEST_VERBOSE'}; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + my ($txnid, $msg); + ($tid, $txnid, $msg) = $ticket->Create( + Subject => 'test', + Queue => $queue->id, + "CustomField-$cfid" => 'asd', + ); + ok $tid, "created ticket"; + diag $msg if $msg && $ENV{'TEST_VERBOSE'}; + + # we use lc as we really don't care about case + # so if later we'll add canonicalization of value + # test should work + is lc $ticket->FirstCustomFieldValue( $cf_name ), + 'asd', 'assigned value of the CF'; +} + +diag "check that values of the CF are case insensetive(asd vs. ASD)" + if $ENV{'TEST_VERBOSE'}; +{ + ok $m->goto_ticket( $tid ), "opened ticket's page"; + $m->follow_link( text => 'Custom Fields' ); + $m->title_like(qr/Modify ticket/i, 'modify ticket'); + $m->content_like(qr/\Q$cf_name/, 'CF on the page'); + + my $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values"); + is lc $value, 'asd', 'correct value is selected'; + $m->submit; + $m->content_unlike(qr/\Q$cf_name\E.*?changed/mi, 'field is not changed'); + + $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values"); + is lc $value, 'asd', 'the same value is still selected'; + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $tid ); + ok $ticket->id, 'loaded the ticket'; + is lc $ticket->FirstCustomFieldValue( $cf_name ), + 'asd', 'value is still the same'; +} + +diag "check that 0 is ok value of the CF" + if $ENV{'TEST_VERBOSE'}; +{ + ok $m->goto_ticket( $tid ), "opened ticket's page"; + $m->follow_link( text => 'Custom Fields' ); + $m->title_like(qr/Modify ticket/i, 'modify ticket'); + $m->content_like(qr/\Q$cf_name/, 'CF on the page'); + + my $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values"); + is lc $value, 'asd', 'correct value is selected'; + $m->select("Object-RT::Ticket-$tid-CustomField-$cfid-Values" => 0 ); + $m->submit; + $m->content_like(qr/\Q$cf_name\E.*?changed/mi, 'field is changed'); + $m->content_unlike(qr/0 is no longer a value for custom field/mi, 'no bad message in results'); + + $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values"); + is lc $value, '0', 'new value is selected'; + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $tid ); + ok $ticket->id, 'loaded the ticket'; + is lc $ticket->FirstCustomFieldValue( $cf_name ), + '0', 'API returns correct value'; +} + +diag "check that we can set empty value when the current is 0" + if $ENV{'TEST_VERBOSE'}; +{ + ok $m->goto_ticket( $tid ), "opened ticket's page"; + $m->follow_link( text => 'Custom Fields' ); + $m->title_like(qr/Modify ticket/i, 'modify ticket'); + $m->content_like(qr/\Q$cf_name/, 'CF on the page'); + + my $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values"); + is lc $value, '0', 'correct value is selected'; + $m->select("Object-RT::Ticket-$tid-CustomField-$cfid-Values" => '' ); + $m->submit; + $m->content_like(qr/0 is no longer a value for custom field/mi, '0 is no longer a value'); + + $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values"); + is $value, '', '(no value) is selected'; + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $tid ); + ok $ticket->id, 'loaded the ticket'; + is $ticket->FirstCustomFieldValue( $cf_name ), + undef, 'API returns correct value'; +} + diff --git a/rt/t/web/command_line.t b/rt/t/web/command_line.t new file mode 100644 index 000000000..3fc279bf3 --- /dev/null +++ b/rt/t/web/command_line.t @@ -0,0 +1,544 @@ +#!/usr/bin/perl -w + +use strict; +use File::Spec (); +use Test::Expect; +use RT::Test tests => 295; +my ($baseurl, $m) = RT::Test->started_ok; + +use RT::User; +use RT::Queue; + +my $rt_tool_path = "$RT::BinPath/rt"; + +# {{{ test configuration options + +# config directives: +# (in $CWD/.rtrc) +# - server <URL> URL to RT server. +# - user <username> RT username. +# - passwd <passwd> RT user's password. +# - query <RT Query> Default RT Query for list action +# - orderby <order> Default RT order for list action +# +# Blank and #-commented lines are ignored. + +# environment variables +# The following environment variables override any corresponding +# values defined in configuration files: +# +# - RTUSER +$ENV{'RTUSER'} = 'root'; +# - RTPASSWD +$ENV{'RTPASSWD'} = 'password'; +# - RTSERVER +$RT::Logger->debug("Connecting to server at ".RT->Config->Get('WebBaseURL')); +$ENV{'RTSERVER'} =RT->Config->Get('WebBaseURL') ; +# - RTDEBUG Numeric debug level. (Set to 3 for full logs.) +$ENV{'RTDEBUG'} = '1'; +# - RTCONFIG Specifies a name other than ".rtrc" for the +# configuration file. +# +# - RTQUERY Default RT Query for rt list +# - RTORDERBY Default order for rt list + + +# }}} + +# {{{ test ticket manipulation + +# create a ticket +expect_run( + command => "$rt_tool_path shell", + prompt => 'rt> ', + quit => 'quit', +); +expect_send(q{create -t ticket set subject='new ticket' add cc=foo@example.com}, "Creating a ticket..."); +expect_like(qr/Ticket \d+ created/, "Created the ticket"); +expect_handle->before() =~ /Ticket (\d+) created/; +my $ticket_id = $1; +ok($ticket_id, "Got ticket id=$ticket_id"); +expect_send(q{create -t ticket set subject='new ticket'}, "Creating a ticket as just a subject..."); +expect_like(qr/Ticket \d+ created/, "Created the ticket"); + +# make sure we can request things as 'rt foo' +expect_send(q{rt create -t ticket set subject='rt ticket'}, "Creating a ticket with 'rt create'..."); +expect_like(qr/Ticket \d+ created/, "Created the ticket"); + +# {{{ test queue manipulation + +# creating queues +expect_send("create -t queue set Name='NewQueue$$'", 'Creating a queue...'); +expect_like(qr/Queue \d+ created/, 'Created the queue'); +expect_handle->before() =~ /Queue (\d+) created/; +my $queue_id = $1; +ok($queue_id, "Got queue id=$queue_id"); +# updating users +expect_send("edit queue/$queue_id set Name='EditedQueue$$'", 'Editing the queue'); +expect_like(qr/Queue $queue_id updated/, 'Edited the queue'); +expect_send("show queue/$queue_id", 'Showing the queue...'); +expect_like(qr/id: queue\/$queue_id/, 'Saw the queue'); +expect_like(qr/Name: EditedQueue$$/, 'Saw the modification'); +TODO: { + todo_skip "Listing non-ticket items doesn't work", 2; + expect_send("list -t queue 'id > 0'", 'Listing the queues...'); + expect_like(qr/$queue_id: EditedQueue$$/, 'Found the queue'); +} + +# }}} + + +# Set up a custom field for editing tests +my $cf = RT::CustomField->new($RT::SystemUser); +my ($val,$msg) = $cf->Create(Name => 'MyCF'.$$, Type => 'FreeformSingle', Queue => $queue_id); +ok($val,$msg); + +my $othercf = RT::CustomField->new($RT::SystemUser); +($val,$msg) = $othercf->Create(Name => 'My CF'.$$, Type => 'FreeformSingle', Queue => $queue_id); +ok($val,$msg); + +my $multiple_cf = RT::CustomField->new($RT::SystemUser); +($val,$msg) = $multiple_cf->Create(Name => 'MultipleCF'.$$, Type => + 'FreeformMultiple', Queue => $queue_id); +ok($val,$msg); + + +# add a comment to ticket + expect_send("comment -m 'comment-$$' $ticket_id", "Adding a comment..."); + expect_like(qr/Message recorded/, "Added the comment"); + ### should test to make sure it actually got added + # add correspondance to ticket (?) + expect_send("correspond -m 'correspond-$$' $ticket_id", "Adding correspondence..."); + expect_like(qr/Message recorded/, "Added the correspondence"); + ### should test to make sure it actually got added + + my $test_email = RT::Test::get_relocatable_file('lorem-ipsum', + (File::Spec->updir(), 'data', 'emails')); + # add attachments to a ticket + # text attachment + check_attachment($test_email); + # binary attachment + check_attachment($RT::MasonComponentRoot.'/NoAuth/images/bplogo.gif'); + +# change a ticket's Owner +expect_send("edit ticket/$ticket_id set owner=root", 'Changing owner...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed owner'); +expect_send("show ticket/$ticket_id -f owner", 'Verifying change...'); +expect_like(qr/Owner: root/, 'Verified change'); +# change a ticket's Requestor +expect_send("edit ticket/$ticket_id set requestors=foo\@example.com", 'Changing Requestor...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed Requestor'); +expect_send("show ticket/$ticket_id -f requestors", 'Verifying change...'); +expect_like(qr/Requestors: foo\@example.com/, 'Verified change'); +# change a ticket's Cc +expect_send("edit ticket/$ticket_id set cc=bar\@example.com", 'Changing Cc...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed Cc'); +expect_send("show ticket/$ticket_id -f cc", 'Verifying change...'); +expect_like(qr/Cc: bar\@example.com/, 'Verified change'); +# change a ticket's priority +expect_send("edit ticket/$ticket_id set priority=10", 'Changing priority...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed priority'); +expect_send("show ticket/$ticket_id -f priority", 'Verifying change...'); +expect_like(qr/Priority: 10/, 'Verified change'); +# move a ticket to a different queue +expect_send("edit ticket/$ticket_id set queue=EditedQueue$$", 'Changing queue...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed queue'); +expect_send("show ticket/$ticket_id -f queue", 'Verifying change...'); +expect_like(qr/Queue: EditedQueue$$/, 'Verified change'); +# cannot move ticket to a nonexistent queue +expect_send("edit ticket/$ticket_id set queue=nonexistent-$$", 'Changing to nonexistent queue...'); +expect_like(qr/queue does not exist/i, 'Errored out'); +expect_send("show ticket/$ticket_id -f queue", 'Verifying lack of change...'); +expect_like(qr/Queue: EditedQueue$$/, 'Verified lack of change'); + +# Test reading and setting custom fields without spaces +expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking initial value'); +expect_like(qr/CF\.{myCF$$}:/i, 'Verified initial empty value (CF-x syntax)'); +expect_send("show ticket/$ticket_id -f CF.{myCF$$}", 'Checking initial value'); +expect_like(qr/CF\.{myCF$$}:/i, 'Verified initial empty value (CF.{x} syntax)'); + +expect_send("edit ticket/$ticket_id set 'CF-myCF$$=VALUE' ", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed cf'); +expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value'); +expect_like(qr/CF\.{myCF$$}: VALUE/i, 'Verified change'); +# Test setting 0 as value of the custom field +expect_send("edit ticket/$ticket_id set 'CF-myCF$$=0' ", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed cf'); +expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value'); +expect_like(qr/CF\.{myCF$$}: 0/i, 'Verified change'); + +expect_send("edit ticket/$ticket_id set 'CF.{myCF$$}=VALUE' ",'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed cf'); +expect_send("show ticket/$ticket_id -f CF.{myCF$$}", 'Checking new value'); +expect_like(qr/CF\.{myCF$$}: VALUE/i, 'Verified change'); +# Test setting 0 as value of the custom field +expect_send("edit ticket/$ticket_id set 'CF.{myCF$$}=0' ", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed cf'); +expect_send("show ticket/$ticket_id -f CF.{myCF$$}", 'Checking new value'); +expect_like(qr/CF\.{myCF$$}: 0/i, 'Verified change'); + +# Test reading and setting custom fields with spaces +expect_send("show ticket/$ticket_id -f 'CF-my CF$$'", 'Checking initial value'); +expect_like(qr/CF\.{my CF$$}:/i, 'Verified change'); +expect_send("edit ticket/$ticket_id set 'CF-my CF$$=VALUE' ", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed cf'); +expect_send("show ticket/$ticket_id -f 'CF-my CF$$'", 'Checking new value'); +expect_like(qr/CF\.{my CF$$}: VALUE/i, 'Verified change'); +expect_send("ls -l 'id = $ticket_id' -f 'CF-my CF$$'", 'Checking new value'); +expect_like(qr/CF\.{my CF$$}: VALUE/i, 'Verified change'); + +expect_send("show ticket/$ticket_id -f 'CF.{my CF$$}'", 'Checking initial value'); +expect_like(qr/CF\.{my CF$$}: VALUE/i, 'Verified change'); +expect_send("edit ticket/$ticket_id set 'CF.{my CF$$}=NEW' ", 'Changing CF...'); +expect_send("show ticket/$ticket_id -f 'CF.{my CF$$}'", 'Checking new value'); +expect_like(qr/CF\.{my CF$$}: NEW/i, 'Verified change'); +expect_send("ls -l 'id = $ticket_id' -f 'CF.{my CF$$}'", 'Checking new value'); +expect_like(qr/CF\.{my CF$$}: NEW/i, 'Verified change'); + +# Test reading and setting single value custom field with commas or quotes +expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking initial value'); +expect_like(qr/CF\.{myCF$$}:/i, 'Verified change'); +expect_send("edit ticket/$ticket_id set CF-myCF$$=1,2,3", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed cf'); +expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value'); +expect_like(qr/CF\.{myCF$$}: 1,2,3/i, 'Verified change'); +expect_send("edit ticket/$ticket_id set CF-myCF$$=\"1's,2,3\"", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed cf'); +expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value'); +expect_like(qr/CF\.{myCF$$}: 1's,2,3/i, 'Verified change'); + +# Test reading and setting custom fields with multiple values +expect_send("show ticket/$ticket_id -f CF-MultipleCF$$", 'Checking initial value'); +expect_like(qr/CF\.{MultipleCF$$}:/i, 'Verified multiple cf change'); +expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=1,2,3 ", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: 1,\s*2,\s*3/i, 'Verified multiple cf change'); +expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=a,b,c ", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: a,\s*b,\s*c/i, 'Verified change'); +expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=a", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'del multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: b,\s*c/i, 'Verified multiple cf change'); +expect_send("edit ticket/$ticket_id add CF.{MultipleCF$$}=o", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: b,\s*c,\s*o/i, 'Verified multiple cf change'); + +expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=\"'a,b,c'\" ", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change'); +expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=a", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change'); + +expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=q{a,b,c}", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change'); +expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=a", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change'); +expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=\"'a,b,c'\"", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: \s*$/i, 'Verified change'); + +expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=\"q{1,2's,3}\"", 'Changing CF...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf'); +expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value'); +expect_like(qr/CF\.{MultipleCF$$}: '1,2\\'s,3'/i, 'Verified change'); + +# ... +# change a ticket's ...[other properties]... +# ... +# stall a ticket +expect_send("edit ticket/$ticket_id set status=stalled", 'Changing status to "stalled"...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed status'); +expect_send("show ticket/$ticket_id -f status", 'Verifying change...'); +expect_like(qr/Status: stalled/, 'Verified change'); +# resolve a ticket +expect_send("edit ticket/$ticket_id set status=resolved", 'Changing status to "resolved"...'); +expect_like(qr/Ticket $ticket_id updated/, 'Changed status'); +expect_send("show ticket/$ticket_id -f status", 'Verifying change...'); +expect_like(qr/Status: resolved/, 'Verified change'); +# try to set status to an illegal value +expect_send("edit ticket/$ticket_id set status=quux", 'Changing status to an illegal value...'); +expect_like(qr/illegal value/i, 'Errored out'); +expect_send("show ticket/$ticket_id -f status", 'Verifying lack of change...'); +expect_like(qr/Status: resolved/, 'Verified change'); + +# }}} + +# {{{ display + +# show ticket list +expect_send("ls -s -t ticket -o +id \"Status='resolved'\"", 'Listing resolved tickets...'); +expect_like(qr/$ticket_id: new ticket/, 'Found our ticket'); +# show ticket list verbosely +expect_send("ls -l -t ticket -o +id \"Status='resolved'\"", 'Listing resolved tickets verbosely...'); +expect_like(qr/id: ticket\/$ticket_id/, 'Found our ticket'); +# show ticket +expect_send("show -s -t ticket $ticket_id", 'Showing our ticket...'); +expect_like(qr/id: ticket\/$ticket_id/, 'Got our ticket'); +# show ticket history +expect_send("show ticket/$ticket_id/history", 'Showing our ticket\'s history...'); +expect_like(qr/Ticket created by root/, 'Got our history'); + +expect_send("show -v ticket/$ticket_id/history", 'Showing our ticket\'s history verbosely...'); +TODO: { + local $TODO = "Cannot show verbose ticket history right now"; + # show ticket history verbosely + expect_like(qr/Ticket created by root/, 'Got our history'); +} +# get attachments from a ticket +expect_send("show -s ticket/$ticket_id/attachments", 'Showing ticket attachments...'); +expect_like(qr/id: ticket\/$ticket_id\/attachments/, 'Got our ticket\'s attachments'); +expect_like(qr/Attachments: \d+: \(Unnamed\) \(\S+ \/ \d+\w+\)/, 'Our ticket has an attachment'); +expect_handle->before() =~ /Attachments: (\d+): \(Unnamed\) \((\S+)/; +my $attachment_id = $1; +my $attachment_type = $2; +ok($attachment_id, "Got attachment id=$attachment_id $attachment_type"); +expect_send("show -s ticket/$ticket_id/attachments/$attachment_id", "Showing attachment $attachment_id..."); +expect_like(qr/ContentType: $attachment_type/, 'Got the attachment'); + +# }}} + +# {{{ test user manipulation + +# creating users +expect_send("create -t user set Name='NewUser$$' EmailAddress='fbar$$\@example.com'", 'Creating a user...'); +expect_like(qr/User \d+ created/, 'Created the user'); +expect_handle->before() =~ /User (\d+) created/; +my $user_id = $1; +ok($user_id, "Got user id=$user_id"); +# updating users +expect_send("edit user/$user_id set Name='EditedUser$$'", 'Editing the user'); +expect_like(qr/User $user_id updated/, 'Edited the user'); +expect_send("show user/$user_id", 'Showing the user...'); +expect_like(qr/id: user\/$user_id/, 'Saw the user'); +expect_like(qr/Name: EditedUser$$/, 'Saw the modification'); +TODO: { + todo_skip "Listing non-ticket items doesn't work", 2; + expect_send("list -t user 'id > 0'", 'Listing the users...'); + expect_like(qr/$user_id: EditedUser$$/, 'Found the user'); +} + +# }}} + +# {{{ test group manipulation + +TODO: { +todo_skip "Group manipulation doesn't work right now", 8; +# creating groups +expect_send("create -t group set Name='NewGroup$$'", 'Creating a group...'); +expect_like(qr/Group \d+ created/, 'Created the group'); +expect_handle->before() =~ /Group (\d+) created/; +my $group_id = $1; +ok($group_id, "Got group id=$group_id"); +# updating groups +expect_send("edit group/$group_id set Name='EditedGroup$$'", 'Editing the group'); +expect_like(qr/Group $group_id updated/, 'Edited the group'); +expect_send("show group/$group_id", 'Showing the group...'); +expect_like(qr/id: group\/$group_id/, 'Saw the group'); +expect_like(qr/Name: EditedGroup$$/, 'Saw the modification'); +TODO: { + local $TODO = "Listing non-ticket items doesn't work"; + expect_send("list -t group 'id > 0'", 'Listing the groups...'); + expect_like(qr/$group_id: EditedGroup$$/, 'Found the group'); +} +} + +# }}} + +TODO: { +todo_skip "Custom field manipulation not yet implemented", 8; +# {{{ test custom field manipulation + +# creating custom fields +expect_send("create -t custom_field set Name='NewCF$$'", 'Creating a custom field...'); +expect_like(qr/Custom Field \d+ created/, 'Created the custom field'); +expect_handle->before() =~ /Custom Field (\d+) created/; +my $cf_id = $1; +ok($cf_id, "Got custom field id=$cf_id"); +# updating custom fields +expect_send("edit cf/$cf_id set Name='EditedCF$$'", 'Editing the custom field'); +expect_like(qr/Custom field $cf_id updated/, 'Edited the custom field'); +expect_send("show cf/$cf_id", 'Showing the queue...'); +expect_like(qr/id: custom_field\/$cf_id/, 'Saw the custom field'); +expect_like(qr/Name: EditedCF$$/, 'Saw the modification'); +TODO: { + todo_skip "Listing non-ticket items doesn't work", 2; + expect_send("list -t custom_field 'id > 0'", 'Listing the CFs...'); + expect_like(qr/$cf_id: EditedCF$$/, 'Found the custom field'); +} +} + +# }}} + +# {{{ test merging tickets +expect_send("create -t ticket set subject='CLIMergeTest1-$$'", 'Creating first ticket to merge...'); +expect_like(qr/Ticket \d+ created/, 'Created first ticket'); +expect_handle->before() =~ /Ticket (\d+) created/; +my $merge_ticket_A = $1; +ok($merge_ticket_A, "Got first ticket to merge id=$merge_ticket_A"); +expect_send("create -t ticket set subject='CLIMergeTest2-$$'", 'Creating second ticket to merge...'); +expect_like(qr/Ticket \d+ created/, 'Created second ticket'); +expect_handle->before() =~ /Ticket (\d+) created/; +my $merge_ticket_B = $1; +ok($merge_ticket_B, "Got second ticket to merge id=$merge_ticket_B"); +expect_send("merge $merge_ticket_B $merge_ticket_A", 'Merging the tickets...'); +expect_like(qr/Merge completed/, 'Merged the tickets'); + +TODO: { + local $TODO = "we generate a spurious warning here"; + $m->no_warnings_ok; +} + +expect_send("show ticket/$merge_ticket_A/history", 'Checking merge on first ticket'); +expect_like(qr/Merged into ticket #$merge_ticket_A by root/, 'Merge recorded in first ticket'); +expect_send("show ticket/$merge_ticket_B/history", 'Checking merge on second ticket'); +expect_like(qr/Merged into ticket #$merge_ticket_A by root/, 'Merge recorded in second ticket'); +# }}} + +# {{{ test taking/stealing tickets +{ + # create a user; give them privileges to take and steal + ### TODO: implement 'grant' in the CLI tool; use that here instead. + ### this breaks the abstraction barrier, like, a lot. + my $steal_user = RT::User->new($RT::SystemUser); + my ($steal_user_id, $msg) = $steal_user->Create( Name => "fooser$$", + EmailAddress => "fooser$$\@localhost", + Privileged => 1, + Password => 'foobar', + ); + ok($steal_user_id, "Created the user? $msg"); + my $steal_queue = RT::Queue->new($RT::SystemUser); + my $steal_queue_id; + ($steal_queue_id, $msg) = $steal_queue->Create( Name => "Steal$$" ); + ok($steal_queue_id, "Got the queue? $msg"); + ok($steal_queue->id, "queue obj has id"); + my $status; + ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'ShowTicket', Object => $steal_queue ); + ok($status, "Gave 'ShowTicket' to our user? $msg"); + ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'OwnTicket', Object => $steal_queue ); + ok($status, "Gave 'OwnTicket' to our user? $msg"); + ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'StealTicket', Object => $steal_queue ); + ok($status, "Gave 'StealTicket' to our user? $msg"); + ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'TakeTicket', Object => $steal_queue ); + ok($status, "Gave 'TakeTicket' to our user? $msg"); + + # create a ticket to take/steal + expect_send("create -t ticket set queue=$steal_queue_id subject='CLIStealTest-$$'", 'Creating ticket to steal...'); + expect_like(qr/Ticket \d+ created/, 'Created ticket'); + expect_handle->before() =~ /Ticket (\d+) created/; + my $steal_ticket_id = $1; + ok($steal_ticket_id, "Got ticket to steal id=$steal_ticket_id"); + + # root takes the ticket + expect_send("take $steal_ticket_id", 'root takes the ticket...'); + expect_like(qr/Owner changed from Nobody to root/, 'root took the ticket'); + expect_quit(); + + # log in as the non-root user + $ENV{'RTUSER'} = "fooser$$"; + $ENV{'RTPASSWD'} = 'foobar'; + expect_run( command => "$rt_tool_path shell", prompt => 'rt> ', quit => 'quit',); + + # user tries to take the ticket, fails + # shouldn't be able to 'take' a ticket which someone else has taken out from + # under you; that should produce an error. should have to explicitly + # 'steal' it back from them. 'steal' can automatically 'take' a ticket, + # though. + expect_send("take $steal_ticket_id", 'user tries to take the ticket...'); + expect_like(qr/You can only take tickets that are unowned/, '...and fails.'); + expect_send("show ticket/$steal_ticket_id -f owner", 'Double-checking...'); + expect_like(qr/Owner: root/, '...no change.'); + + # user steals the ticket + expect_send("steal $steal_ticket_id", 'user tries to *steal* the ticket...'); + expect_like(qr/Owner changed from root to fooser$$/, '...and succeeds!'); + expect_send("show ticket/$steal_ticket_id -f owner", 'Double-checking...'); + expect_like(qr/Owner: fooser$$/, '...yup, it worked.'); + expect_quit(); + + # log back in as root + $ENV{'RTUSER'} = 'root'; + $ENV{'RTPASSWD'} = 'password'; + expect_run( command => "$rt_tool_path shell", prompt => 'rt> ', quit => 'quit',); + + # root steals the ticket back + expect_send("steal $steal_ticket_id", 'root steals the ticket back...'); + expect_like(qr/Owner changed from fooser$$ to root/, '...and succeeds.'); +} +# }}} + +# {{{ test ticket linking + my @link_relns = ( 'DependsOn', 'DependedOnBy', 'RefersTo', 'ReferredToBy', + 'MemberOf', 'HasMember', ); + my %display_relns = map { $_ => $_ } @link_relns; + $display_relns{HasMember} = 'Members'; + + my $link1_id = ok_create_ticket( "LinkTicket1-$$" ); + my $link2_id = ok_create_ticket( "LinkTicket2-$$" ); + + foreach my $reln (@link_relns) { + # create link + expect_send("link $link1_id $reln $link2_id", "Link by $reln..."); + expect_like(qr/Created link $link1_id $reln $link2_id/, 'Linked'); + expect_send("show -s ticket/$link1_id/links", "Checking creation of $reln..."); + expect_like(qr/$display_relns{$reln}: [\w\d\.\-]+:\/\/[\w\d\.]+\/ticket\/$link2_id/, "Created link $reln"); + + # delete link + expect_send("link -d $link1_id $reln $link2_id", "Delete $reln..."); + expect_like(qr/Deleted link $link1_id $reln $link2_id/, 'Deleted'); + expect_send("show ticket/$link1_id/links", "Checking removal of $reln..."); + ok( expect_handle->before() !~ /\Q$display_relns{$reln}: \E[\w\d\.\-]+:\/\/[w\d\.]+\/ticket\/$link2_id/, "Removed link $reln" ); + #expect_unlike(qr/\Q$reln: \E[\w\d\.]+\Q://\E[w\d\.]+\/ticket\/$link2_id/, "Removed link $reln"); + + } +# }}} + +expect_quit(); # We need to do this ourselves, so that we quit + # *before* we tear down the webserver. + +# helper function +sub ok_create_ticket { + my $subject = shift; + + expect_send("create -t ticket set subject='$subject'", 'Creating ticket...'); + expect_like(qr/Ticket \d+ created/, "Created ticket '$subject'"); + expect_handle->before() =~ /Ticket (\d+) created/; + my $id = $1; + ok($id, "Got ticket id=$id"); + + return $id; +} + +# wrap up all the file handling stuff for attachment testing +sub check_attachment { + my $attachment_path = shift; + (my $filename = $attachment_path) =~ s/.*\/(.*)$/$1/; + expect_send("comment -m 'attach file' -a $attachment_path $ticket_id", "Adding an attachment ($filename)"); + expect_like(qr/Message recorded/, "Added the attachment"); + expect_send("show ticket/$ticket_id/attachments","Finding Attachment"); + my $attachment_regex = qr/(\d+):\s+$filename/; + expect_like($attachment_regex,"Attachment Uploaded"); + expect_handle->before() =~ $attachment_regex; + my $attachment_id = $1; + expect_send("show ticket/$ticket_id/attachments/$attachment_id/content","Fetching Attachment"); + open (my $fh, $attachment_path) or die "Can't open $attachment_path: $!"; + my $attachment_content = do { local($/); <$fh> }; + close $fh; + chomp $attachment_content; + expect_is($attachment_content,"Attachment contains original text"); +} + + + +1; diff --git a/rt/t/web/command_line_with_unknown_field.t b/rt/t/web/command_line_with_unknown_field.t new file mode 100644 index 000000000..9a7ec7acd --- /dev/null +++ b/rt/t/web/command_line_with_unknown_field.t @@ -0,0 +1,34 @@ +#!/usr/bin/perl -w + +use strict; +use File::Spec (); +use Test::Expect; +use RT::Test tests => 10; +my ($baseurl, $m) = RT::Test->started_ok; +my $rt_tool_path = "$RT::BinPath/rt"; + +$ENV{'RTUSER'} = 'root'; +$ENV{'RTPASSWD'} = 'password'; +$RT::Logger->debug("Connecting to server at ".RT->Config->Get('WebBaseURL')); +$ENV{'RTSERVER'} =RT->Config->Get('WebBaseURL') ; +$ENV{'RTDEBUG'} = '1'; + +expect_run( + command => "$rt_tool_path shell", + prompt => 'rt> ', + quit => 'quit', +); +expect_send(q{create -t ticket set subject='new ticket' add cc=foo@example.com}, "Creating a ticket..."); +expect_like(qr/Ticket \d+ created/, "Created the ticket"); +expect_handle->before() =~ /Ticket (\d+) created/; +my $ticket_id = $1; + +expect_send("edit ticket/$ticket_id set marge=simpson", 'set unknown field'); +expect_like(qr/marge: Unknown field/, 'marge is unknown field'); +expect_like(qr/marge: simpson/, 'the value we set for marge is shown too'); + +expect_send("edit ticket/$ticket_id set homer=simpson", 'set unknown field'); +expect_like(qr/homer: Unknown field/, 'homer is unknown field'); +expect_like(qr/homer: simpson/, 'the value we set for homer is shown too'); + +expect_quit(); diff --git a/rt/t/web/compilation_errors.t b/rt/t/web/compilation_errors.t new file mode 100644 index 000000000..46a862868 --- /dev/null +++ b/rt/t/web/compilation_errors.t @@ -0,0 +1,68 @@ +#!/usr/bin/perl + +use strict; +use Test::More; +use File::Find; +BEGIN { + sub wanted { + -f && /\.html$/ && $_ !~ /Logout.html$/; + } + my $tests = 7; + find( sub { wanted() and $tests += 4 }, 'share/html/' ); + plan tests => $tests; +} + + +use HTTP::Request::Common; +use HTTP::Cookies; +use LWP; +use Encode; + +my $cookie_jar = HTTP::Cookies->new; + +use RT::Test; +my ($baseurl, $agent) = RT::Test->started_ok; + +# give the agent a place to stash the cookies +$agent->cookie_jar($cookie_jar); + +# get the top page +my $url = $agent->rt_base_url; +diag "Base URL is '$url'" if $ENV{TEST_VERBOSE}; +$agent->get($url); + +is ($agent->{'status'}, 200, "Loaded a page"); + +# {{{ test a login + +# follow the link marked "Login" + +ok($agent->{form}->find_input('user')); + +ok($agent->{form}->find_input('pass')); +like ($agent->{'content'} , qr/username:/i); +$agent->field( 'user' => 'root' ); +$agent->field( 'pass' => 'password' ); +# the field isn't named, so we have to click link 0 +$agent->click(0); +is($agent->{'status'}, 200, "Fetched the page ok"); +like( $agent->{'content'} , qr/Logout/i, "Found a logout link"); + + +find ( sub { wanted() and test_get($File::Find::name) } , 'share/html/'); + +sub test_get { + my $file = shift; + + $file =~ s#^share/html/##; + diag( "testing $url/$file" ) if $ENV{TEST_VERBOSE}; + ok ($agent->get("$url/$file", "GET $url/$file"), "Can Get $url/$file"); + is ($agent->{'status'}, 200, "Loaded $file"); +# ok( $agent->{'content'} =~ /Logout/i, "Found a logout link on $file "); + ok( $agent->{'content'} !~ /Not logged in/i, "Still logged in for $file"); + ok( $agent->{'content'} !~ /raw error/i, "Didn't get a Mason compilation error on $file"); +} + +# }}} + +1; diff --git a/rt/t/web/config_tab_right.t b/rt/t/web/config_tab_right.t new file mode 100644 index 000000000..4dc9ec082 --- /dev/null +++ b/rt/t/web/config_tab_right.t @@ -0,0 +1,41 @@ +#!/usr/bin/perl -w +use strict; +use warnings; + +use RT::Test tests => 8; + +my ($uname, $upass, $user) = ('tester', 'tester'); +{ + $user = RT::User->new($RT::SystemUser); + my ($status, $msg) = $user->Create( + Name => $uname, + Password => $upass, + Disabled => 0, + Privileged => 1, + ); + ok($status, 'created a user'); +} + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login($uname, $upass), "logged in"; + +{ + $m->content_unlike(qr/Configuration/, 'no configuration'); + $m->get('/Admin/'); + is $m->status, 403, 'no access to /Admin/'; +} + +RT::Test->set_rights( + { Principal => $user->PrincipalObj, + Right => [qw(ShowConfigTab)], + }, +); + +{ + $m->get('/'); + $m->content_like(qr/Configuration/, 'configuration is there'); + + $m->follow_link_ok({text => 'Configuration'}); + is $m->status, 200, 'user has access to /Admin/'; +} + diff --git a/rt/t/web/crypt-gnupg.t b/rt/t/web/crypt-gnupg.t new file mode 100644 index 000000000..fb28c887c --- /dev/null +++ b/rt/t/web/crypt-gnupg.t @@ -0,0 +1,446 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 94; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use RT::Action::SendEmail; + +eval 'use GnuPG::Interface; 1' or plan skip_all => 'GnuPG required.'; + +RT::Test->set_mail_catcher; + +RT->Config->Set( CommentAddress => 'general@example.com'); +RT->Config->Set( CorrespondAddress => 'general@example.com'); +RT->Config->Set( DefaultSearchResultFormat => qq{ + '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__id__</a></B>/TITLE:#', + '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject', + 'OO-__OwnerName__-O', + 'OR-__Requestors__-O', + 'KO-__KeyOwnerName__-K', + 'KR-__KeyRequestors__-K', + Status}); + +use File::Spec (); +use Cwd; +use File::Temp qw(tempdir); +my $homedir = tempdir( CLEANUP => 1 ); + +use_ok('RT::Crypt::GnuPG'); + +RT->Config->Set( 'GnuPG', + Enable => 1, + OutgoingMessagesFormat => 'RFC' ); + +RT->Config->Set( 'GnuPGOptions', + homedir => $homedir, + passphrase => 'recipient', + 'no-permission-warning' => undef, + 'trust-model' => 'always'); +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + +RT::Test->import_gnupg_key('recipient@example.com', 'public'); +RT::Test->import_gnupg_key('recipient@example.com', 'secret'); +RT::Test->import_gnupg_key('general@example.com', 'public'); +RT::Test->import_gnupg_key('general@example.com', 'secret'); +RT::Test->import_gnupg_key('general@example.com.2', 'public'); +RT::Test->import_gnupg_key('general@example.com.2', 'secret'); + +ok(my $user = RT::User->new($RT::SystemUser)); +ok($user->Load('root'), "Loaded user 'root'"); +$user->SetEmailAddress('recipient@example.com'); + +my $queue = RT::Test->load_or_create_queue( + Name => 'General', + CorrespondAddress => 'general@example.com', +); +ok $queue && $queue->id, 'loaded or created queue'; +my $qid = $queue->id; + +RT::Test->set_rights( + Principal => 'Everyone', + Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ModifyTicket'], +); + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + +$m->get_ok("/Admin/Queues/Modify.html?id=$qid"); +$m->form_with_fields('Sign', 'Encrypt'); +$m->field(Encrypt => 1); +$m->submit; + +RT::Test->clean_caught_mails; + +$m->goto_create_ticket( $queue ); +$m->form_name('TicketCreate'); +$m->field('Subject', 'Encryption test'); +$m->field('Content', 'Some content'); +ok($m->value('Encrypt', 2), "encrypt tick box is checked"); +ok(!$m->value('Sign', 2), "sign tick box is unchecked"); +$m->submit; +is($m->status, 200, "request successful"); + +$m->get($baseurl); # ensure that the mail has been processed + +my @mail = RT::Test->fetch_caught_mails; +ok(@mail, "got some mail"); + +$user->SetEmailAddress('general@example.com'); +for my $mail (@mail) { + unlike $mail, qr/Some content/, "outgoing mail was encrypted"; + + my ($content_type) = $mail =~ /^(Content-Type: .*)/m; + my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m; + my $body = strip_headers($mail); + + $mail = << "MAIL"; +Subject: RT mail sent back into RT +From: general\@example.com +To: recipient\@example.com +$mime_version +$content_type + +$body +MAIL + + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + is ($tick->Subject, + "RT mail sent back into RT", + "Correct subject" + ); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is( $msg->GetHeader('X-RT-Privacy'), + 'PGP', + "RT's outgoing mail has crypto" + ); + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Success', + "RT's outgoing mail looks encrypted" + ); + + like($attachments[0]->Content, qr/Some content/, "RT's mail includes copy of ticket text"); + like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name"); +} + +$m->get("$baseurl/Admin/Queues/Modify.html?id=$qid"); +$m->form_with_fields('Sign', 'Encrypt'); +$m->field(Encrypt => undef); +$m->field(Sign => 1); +$m->submit; + +RT::Test->clean_caught_mails; + +$m->goto_create_ticket( $queue ); +$m->form_name('TicketCreate'); +$m->field('Subject', 'Signing test'); +$m->field('Content', 'Some other content'); +ok(!$m->value('Encrypt', 2), "encrypt tick box is unchecked"); +ok($m->value('Sign', 2), "sign tick box is checked"); +$m->submit; +is($m->status, 200, "request successful"); + +$m->get($baseurl); # ensure that the mail has been processed + +@mail = RT::Test->fetch_caught_mails; +ok(@mail, "got some mail"); +for my $mail (@mail) { + like $mail, qr/Some other content/, "outgoing mail was not encrypted"; + like $mail, qr/-----BEGIN PGP SIGNATURE-----[\s\S]+-----END PGP SIGNATURE-----/, "data has some kind of signature"; + + my ($content_type) = $mail =~ /^(Content-Type: .*)/m; + my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m; + my $body = strip_headers($mail); + + $mail = << "MAIL"; +Subject: More RT mail sent back into RT +From: general\@example.com +To: recipient\@example.com +$mime_version +$content_type + +$body +MAIL + + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + is ($tick->Subject, + "More RT mail sent back into RT", + "Correct subject" + ); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is( $msg->GetHeader('X-RT-Privacy'), + 'PGP', + "RT's outgoing mail has crypto" + ); + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Not encrypted', + "RT's outgoing mail looks unencrypted" + ); + is( $msg->GetHeader('X-RT-Incoming-Signature'), + 'general <general@example.com>', + "RT's outgoing mail looks signed" + ); + + like($attachments[0]->Content, qr/Some other content/, "RT's mail includes copy of ticket text"); + like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name"); +} + +$m->get("$baseurl/Admin/Queues/Modify.html?id=$qid"); +$m->form_with_fields('Sign', 'Encrypt'); +$m->field(Encrypt => 1); +$m->field(Sign => 1); +$m->submit; + +RT::Test->clean_caught_mails; + + +$m->goto_create_ticket( $queue ); +$m->form_name('TicketCreate'); +$m->field('Subject', 'Crypt+Sign test'); +$m->field('Content', 'Some final? content'); +ok($m->value('Encrypt', 2), "encrypt tick box is checked"); +ok($m->value('Sign', 2), "sign tick box is checked"); +$m->submit; +is($m->status, 200, "request successful"); + +$m->get($baseurl); # ensure that the mail has been processed + +@mail = RT::Test->fetch_caught_mails; +ok(@mail, "got some mail"); +for my $mail (@mail) { + unlike $mail, qr/Some other content/, "outgoing mail was encrypted"; + + my ($content_type) = $mail =~ /^(Content-Type: .*)/m; + my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m; + my $body = strip_headers($mail); + + $mail = << "MAIL"; +Subject: Final RT mail sent back into RT +From: general\@example.com +To: recipient\@example.com +$mime_version +$content_type + +$body +MAIL + + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + is ($tick->Subject, + "Final RT mail sent back into RT", + "Correct subject" + ); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is( $msg->GetHeader('X-RT-Privacy'), + 'PGP', + "RT's outgoing mail has crypto" + ); + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Success', + "RT's outgoing mail looks encrypted" + ); + is( $msg->GetHeader('X-RT-Incoming-Signature'), + 'general <general@example.com>', + "RT's outgoing mail looks signed" + ); + + like($attachments[0]->Content, qr/Some final\? content/, "RT's mail includes copy of ticket text"); + like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name"); +} + +RT::Test->clean_caught_mails; + +$m->goto_create_ticket( $queue ); +$m->form_name('TicketCreate'); +$m->field('Subject', 'Test crypt-off on encrypted queue'); +$m->field('Content', 'Thought you had me figured out didya'); +$m->field(Encrypt => undef, 2); # turn off encryption +ok(!$m->value('Encrypt', 2), "encrypt tick box is now unchecked"); +ok($m->value('Sign', 2), "sign tick box is still checked"); +$m->submit; +is($m->status, 200, "request successful"); + +$m->get($baseurl); # ensure that the mail has been processed + +@mail = RT::Test->fetch_caught_mails; +ok(@mail, "got some mail"); +for my $mail (@mail) { + like $mail, qr/Thought you had me figured out didya/, "outgoing mail was unencrypted"; + + my ($content_type) = $mail =~ /^(Content-Type: .*)/m; + my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m; + my $body = strip_headers($mail); + + $mail = << "MAIL"; +Subject: Post-final! RT mail sent back into RT +From: general\@example.com +To: recipient\@example.com +$mime_version +$content_type + +$body +MAIL + + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + is ($tick->Subject, + "Post-final! RT mail sent back into RT", + "Correct subject" + ); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is( $msg->GetHeader('X-RT-Privacy'), + 'PGP', + "RT's outgoing mail has crypto" + ); + is( $msg->GetHeader('X-RT-Incoming-Encryption'), + 'Not encrypted', + "RT's outgoing mail looks unencrypted" + ); + is( $msg->GetHeader('X-RT-Incoming-Signature'), + 'general <general@example.com>', + "RT's outgoing mail looks signed" + ); + + like($attachments[0]->Content, qr/Thought you had me figured out didya/, "RT's mail includes copy of ticket text"); + like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name"); +} + +sub strip_headers +{ + my $mail = shift; + $mail =~ s/.*?\n\n//s; + return $mail; +} + +# now test the OwnerNameKey and RequestorsKey fields + +my $nokey = RT::Test->load_or_create_user(Name => 'nokey', EmailAddress => 'nokey@example.com'); +$nokey->PrincipalObj->GrantRight(Right => 'CreateTicket'); +$nokey->PrincipalObj->GrantRight(Right => 'OwnTicket'); + +my $tick = RT::Ticket->new( $RT::SystemUser ); +$tick->Create(Subject => 'owner lacks pubkey', Queue => 'general', + Owner => $nokey); +ok(my $id = $tick->id, 'created ticket for owner-without-pubkey'); + +$tick = RT::Ticket->new( $RT::SystemUser ); +$tick->Create(Subject => 'owner has pubkey', Queue => 'general', + Owner => 'root'); +ok($id = $tick->id, 'created ticket for owner-with-pubkey'); + +my $mail = << "MAIL"; +Subject: Nokey requestor +From: nokey\@example.com +To: general\@example.com + +hello +MAIL + +((my $status), $id) = RT::Test->send_via_mailgate($mail); +is ($status >> 8, 0, "The mail gateway exited normally"); +ok ($id, "got id of a newly created ticket - $id"); + +$tick = RT::Ticket->new( $RT::SystemUser ); +$tick->Load( $id ); +ok ($tick->id, "loaded ticket #$id"); + +is ($tick->Subject, + "Nokey requestor", + "Correct subject" +); + +# test key selection +my $key1 = "EC1E81E7DC3DB42788FB0E4E9FA662C06DE22FC2"; +my $key2 = "75E156271DCCF02DDD4A7A8CDF651FA0632C4F50"; + +ok($user = RT::User->new($RT::SystemUser)); +ok($user->Load('root'), "Loaded user 'root'"); +is($user->PreferredKey, $key1, "preferred key is set correctly"); +$m->get("$baseurl/Prefs/Other.html"); +like($m->content, qr/Preferred key/, "preferred key option shows up in preference"); + +# XXX: mech doesn't let us see the current value of the select, apparently +like($m->content, qr/$key1/, "first key shows up in preferences"); +like($m->content, qr/$key2/, "second key shows up in preferences"); +like($m->content, qr/$key1.*?$key2/s, "first key shows up before the second"); + +$m->form_number(3); +$m->select("PreferredKey" => $key2); +$m->submit; + +ok($user = RT::User->new($RT::SystemUser)); +ok($user->Load('root'), "Loaded user 'root'"); +is($user->PreferredKey, $key2, "preferred key is set correctly to the new value"); + +$m->get("$baseurl/Prefs/Other.html"); +like($m->content, qr/Preferred key/, "preferred key option shows up in preference"); + +# XXX: mech doesn't let us see the current value of the select, apparently +like($m->content, qr/$key2/, "second key shows up in preferences"); +like($m->content, qr/$key1/, "first key shows up in preferences"); +like($m->content, qr/$key2.*?$key1/s, "second key (now preferred) shows up before the first"); + +# test that the new fields work +$m->get("$baseurl/Search/Simple.html?q=General"); +my $content = $m->content; +$content =~ s/(/(/g; +$content =~ s/)/)/g; + +like($content, qr/OO-Nobody-O/, "original OwnerName untouched"); +like($content, qr/OO-nokey-O/, "original OwnerName untouched"); +like($content, qr/OO-root-O/, "original OwnerName untouched"); + +like($content, qr/OR-recipient\@example.com-O/, "original Requestors untouched"); +like($content, qr/OR-nokey\@example.com-O/, "original Requestors untouched"); + +like($content, qr/KO-root-K/, "KeyOwnerName does not issue no-pubkey warning for recipient"); +like($content, qr/KO-nokey \(no pubkey!\)-K/, "KeyOwnerName issues no-pubkey warning for root"); +like($content, qr/KO-Nobody \(no pubkey!\)-K/, "KeyOwnerName issues no-pubkey warning for nobody"); + +like($content, qr/KR-recipient\@example.com-K/, "KeyRequestors does not issue no-pubkey warning for recipient\@example.com"); +like($content, qr/KR-general\@example.com-K/, "KeyRequestors does not issue no-pubkey warning for general\@example.com"); +like($content, qr/KR-nokey\@example.com \(no pubkey!\)-K/, "KeyRequestors DOES issue no-pubkey warning for nokey\@example.com"); + diff --git a/rt/t/web/custom_frontpage.t b/rt/t/web/custom_frontpage.t new file mode 100644 index 000000000..45a390ab0 --- /dev/null +++ b/rt/t/web/custom_frontpage.t @@ -0,0 +1,61 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 7; +my ($baseurl, $m) = RT::Test->started_ok; + +my $url = $m->rt_base_url; + +my $user_obj = RT::User->new($RT::SystemUser); +my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com'); +ok($ret, 'ACL test user creation'); +$user_obj->SetName('customer'); +$user_obj->SetPrivileged(1); +($ret, $msg) = $user_obj->SetPassword('customer'); +$user_obj->PrincipalObj->GrantRight(Right => 'LoadSavedSearch'); +$user_obj->PrincipalObj->GrantRight(Right => 'EditSavedSearch'); +$user_obj->PrincipalObj->GrantRight(Right => 'CreateSavedSearch'); +$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf'); + +ok $m->login( customer => 'customer' ), "logged in"; + +$m->get ( $url."Search/Build.html"); + +#create a saved search +$m->form_name ('BuildQuery'); + +$m->field ( "ValueOfAttachment" => 'stupid'); +$m->field ( "SavedSearchDescription" => 'stupid tickets'); +$m->click_button (name => 'SavedSearchSave'); + +$m->get ( $url.'Prefs/MyRT.html' ); +$m->content_like (qr/stupid tickets/, 'saved search listed in rt at a glance items'); + +ok $m->login, 'we did log in as root'; + +$m->get ( $url.'Prefs/MyRT.html' ); +$m->form_name ('SelectionBox-body'); +# can't use submit form for mutli-valued select as it uses set_fields +$m->field ('body-Selected' => ['component-QuickCreate', 'system-Unowned Tickets', 'system-My Tickets']); +$m->click_button (name => 'remove'); +$m->form_name ('SelectionBox-body'); +#$m->click_button (name => 'body-Save'); +$m->get ( $url ); +$m->content_lacks ('highest priority tickets', 'remove everything from body pane'); + +$m->get ( $url.'Prefs/MyRT.html' ); +$m->form_name ('SelectionBox-body'); +$m->field ('body-Available' => ['component-QuickCreate', 'system-Unowned Tickets', 'system-My Tickets']); +$m->click_button (name => 'add'); + +$m->form_name ('SelectionBox-body'); +$m->field ('body-Selected' => ['component-QuickCreate']); +$m->click_button (name => 'movedown'); + +$m->form_name ('SelectionBox-body'); +$m->click_button (name => 'movedown'); + +$m->form_name ('SelectionBox-body'); +#$m->click_button (name => 'body-Save'); +$m->get ( $url ); +$m->content_like (qr'highest priority tickets', 'adds them back'); diff --git a/rt/t/web/custom_search.t b/rt/t/web/custom_search.t new file mode 100644 index 000000000..05cfdab60 --- /dev/null +++ b/rt/t/web/custom_search.t @@ -0,0 +1,84 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 11; +my ($baseurl, $m) = RT::Test->started_ok; +my $url = $m->rt_base_url; + +# reset preferences for easier test? + + + +my $t = RT::Ticket->new($RT::SystemUser); +$t->Create(Subject => 'for custom search'.$$, Queue => 'general', + Owner => 'root', Requestor => 'customsearch@localhost'); +ok(my $id = $t->id, 'created ticket for custom search'); + +ok $m->login, 'logged in'; + +my $t_link = $m->find_link( text => "for custom search".$$ ); +like ($t_link->url, qr/$id/, 'link to the ticket we created'); + +$m->content_lacks ('customsearch@localhost', 'requestor not displayed '); +$m->get ( $url.'Prefs/MyRT.html' ); +my $cus_hp = $m->find_link( text => "My Tickets" ); +my $cus_qs = $m->find_link( text => "Quick search" ); +$m->get ($cus_hp); +$m->content_like (qr'highest priority tickets'); + +# add Requestor to the fields +$m->form_name ('BuildQuery'); +# can't use submit form for mutli-valued select as it uses set_fields +$m->field (SelectDisplayColumns => ['Requestors']); +$m->click_button (name => 'AddCol') ; + +$m->form_name ('BuildQuery'); +$m->click_button (name => 'Save'); + +$m->get( $url ); +$m->content_contains ('customsearch@localhost', 'requestor now displayed '); + + +# now remove Requestor from the fields +$m->get ($cus_hp); + +$m->form_name ('BuildQuery'); + +my $cdc = $m->current_form->find_input('CurrentDisplayColumns'); +my ($requestor_value) = grep { /Requestor/ } $cdc->possible_values; +ok($requestor_value, "got the requestor value"); + +$m->field (CurrentDisplayColumns => $requestor_value); +$m->click_button (name => 'RemoveCol') ; + +$m->form_name ('BuildQuery'); +$m->click_button (name => 'Save'); + +$m->get( $url ); +$m->content_lacks ('customsearch@localhost', 'requestor not displayed '); + + +# try to disable General from quick search + +# Note that there's a small problem in the current implementation, +# since ticked quese are wanted, we do the invesrsion. So any +# queue added during the quicksearch setting will be unticked. +my $nlinks = $#{$m->find_all_links( text => "General" )}; +$m->get ($cus_qs); +$m->form_name ('Preferences'); +$m->untick('Want-General', '1'); +$m->click_button (name => 'Save'); + +$m->get( $url ); +is ($#{$m->find_all_links( text => "General" )}, $nlinks - 1, + 'General gone from quicksearch list'); + +# get it back +$m->get ($cus_qs); +$m->form_name ('Preferences'); +$m->tick('Want-General', '1'); +$m->click_button (name => 'Save'); + +$m->get( $url ); +is ($#{$m->find_all_links( text => "General" )}, $nlinks, + 'General back in quicksearch list'); diff --git a/rt/t/web/dashboard_with_deleted_saved_search.t b/rt/t/web/dashboard_with_deleted_saved_search.t new file mode 100644 index 000000000..328095aaf --- /dev/null +++ b/rt/t/web/dashboard_with_deleted_saved_search.t @@ -0,0 +1,89 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 18; +my ( $url, $m ) = RT::Test->started_ok; +ok( $m->login, 'logged in' ); + +# create a saved search +$m->get_ok( $url . "/Search/Build.html?Query=" . 'id=1' ); + +$m->submit_form( + form_name => 'BuildQuery', + fields => { SavedSearchDescription => 'foo', }, + button => 'SavedSearchSave', +); + +my ( $search_uri, $user_id, $search_id ) = + $m->content =~ /value="(RT::User-(\d+)-SavedSearch-(\d+))"/; +$m->submit_form( + form_name => 'BuildQuery', + fields => { SavedSearchLoad => $search_uri }, + button => 'SavedSearchSave', +); + +$m->content_like( qr/name="SavedSearchDelete"\s+value="Delete"/, + 'found Delete button' ); +$m->content_like( + qr/name="SavedSearchDescription"\s+value="foo"/, + 'found Description input with the value filled' +); + +# create a dashboard with the created search + +$m->get_ok( $url . "/Dashboards/Modify.html?Create=1" ); +$m->submit_form( + form_name => 'ModifyDashboard', + fields => { Name => 'bar' }, +); + +$m->content_like( qr/Saved dashboard bar/i, 'dashboard saved' ); +my $dashboard_queries_link = $m->find_link( text_regex => qr/Queries/ ); +my ( $dashboard_id ) = $dashboard_queries_link->url =~ /id=(\d+)/; + +$m->get_ok( $url . "/Dashboards/Queries.html?id=$dashboard_id" ); + +$m->content_lacks( 'value="Update"', 'no update button' ); + +$m->submit_form( + form_name => 'Dashboard-Searches-body', + fields => + { 'Searches-body-Available' => "search-$search_id-RT::User-$user_id" }, + button => 'add', +); + +$m->content_like( qr/Dashboard updated/i, 'added search foo to dashboard bar' ); + +# delete the created search + +$m->get_ok( $url . "/Search/Build.html?Query=" . 'id=1' ); +$m->submit_form( + form_name => 'BuildQuery', + fields => { SavedSearchLoad => $search_uri }, +); +$m->submit_form( + form_name => 'BuildQuery', + button => 'SavedSearchDelete', +); + +$m->content_lacks( $search_uri, 'deleted search foo' ); + +# here is what we really want to test + +$m->get_ok( $url . "/Dashboards/Queries.html?id=$dashboard_id" ); +$m->content_like( qr/Deleted queries/i, 'found deleted message' ); + +# Update button shows so we can update the deleted search easily +$m->content_contains( 'value="Update"', 'found update button' ); + +$m->submit_form( + form_name => 'Dashboard-Searches-body', + button => 'update', +); + +$m->content_unlike( qr/Deleted queries/i, 'deleted message is gone' ); +$m->content_lacks( 'value="Update"', 'update button is gone too' ); + +$m->get_warnings; # we'll get a lot of warnings because the deleted search + diff --git a/rt/t/web/dashboards-groups.t b/rt/t/web/dashboards-groups.t new file mode 100644 index 000000000..cbf1d6a9f --- /dev/null +++ b/rt/t/web/dashboards-groups.t @@ -0,0 +1,102 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 40; +my ($baseurl, $m) = RT::Test->started_ok; + +my $url = $m->rt_base_url; + +# create user and queue {{{ +my $user_obj = RT::User->new($RT::SystemUser); +my ($ok, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com'); +ok($ok, 'ACL test user creation'); +$user_obj->SetName('customer'); +$user_obj->SetPrivileged(1); +($ok, $msg) = $user_obj->SetPassword('customer'); +$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf'); +my $currentuser = RT::CurrentUser->new($user_obj); + +my $queue = RT::Queue->new($RT::SystemUser); +$queue->Create(Name => 'SearchQueue'.$$); + +$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $queue) + for qw/SeeQueue ShowTicket OwnTicket/; + +# grant the user all these rights so we can make sure that the group rights +# are checked and not these as well +$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $RT::System) + for qw/SubscribeDashboard CreateOwnDashboard SeeOwnDashboard ModifyOwnDashboard DeleteOwnDashboard/; +# }}} +# create and test groups (outer < inner < user) {{{ +my $inner_group = RT::Group->new($RT::SystemUser); +($ok, $msg) = $inner_group->CreateUserDefinedGroup(Name => "inner", Description => "inner group"); +ok($ok, "created inner group: $msg"); + +my $outer_group = RT::Group->new($RT::SystemUser); +($ok, $msg) = $outer_group->CreateUserDefinedGroup(Name => "outer", Description => "outer group"); +ok($ok, "created outer group: $msg"); + +($ok, $msg) = $outer_group->AddMember($inner_group->PrincipalId); +ok($ok, "added inner as a member of outer: $msg"); + +($ok, $msg) = $inner_group->AddMember($user_obj->PrincipalId); +ok($ok, "added user as a member of member: $msg"); + +ok($outer_group->HasMember($inner_group->PrincipalId), "outer has inner"); +ok(!$outer_group->HasMember($user_obj->PrincipalId), "outer doesn't have user directly"); +ok($outer_group->HasMemberRecursively($inner_group->PrincipalId), "outer has inner recursively"); +ok($outer_group->HasMemberRecursively($user_obj->PrincipalId), "outer has user recursively"); + +ok(!$inner_group->HasMember($outer_group->PrincipalId), "inner doesn't have outer"); +ok($inner_group->HasMember($user_obj->PrincipalId), "inner has user"); +ok(!$inner_group->HasMemberRecursively($outer_group->PrincipalId), "inner doesn't have outer, even recursively"); +ok($inner_group->HasMemberRecursively($user_obj->PrincipalId), "inner has user recursively"); +# }}} + +ok $m->login(customer => 'customer'), "logged in"; + +$m->get_ok("$url/Dashboards"); + +$m->follow_link_ok({text => "New"}); +$m->form_name('ModifyDashboard'); +is_deeply([$m->current_form->find_input('Privacy')->possible_values], ["RT::User-" . $user_obj->Id], "the only selectable privacy is user"); +$m->content_lacks('Delete', "Delete button hidden because we are creating"); + +$user_obj->PrincipalObj->GrantRight(Right => 'CreateGroupDashboard', Object => $inner_group); + +$m->follow_link_ok({text => "New"}); +$m->form_name('ModifyDashboard'); +is_deeply([$m->current_form->find_input('Privacy')->possible_values], ["RT::User-" . $user_obj->Id, "RT::Group-" . $inner_group->Id], "the only selectable privacies are user and inner group (not outer group)"); +$m->field("Name" => 'inner dashboard'); +$m->field("Privacy" => "RT::Group-" . $inner_group->Id); +$m->content_lacks('Delete', "Delete button hidden because we are creating"); + +$m->click_button(value => 'Create'); +$m->content_lacks("No permission to create dashboards"); +$m->content_contains("Saved dashboard inner dashboard"); +$m->content_lacks('Delete', "Delete button hidden because we lack DeleteDashboard"); + +my $dashboard = RT::Dashboard->new($currentuser); +my ($id) = $m->content =~ /name="id" value="(\d+)"/; +ok($id, "got an ID, $id"); +$dashboard->LoadById($id); +is($dashboard->Name, "inner dashboard"); + +is($dashboard->Privacy, 'RT::Group-' . $inner_group->Id, "correct privacy"); +is($dashboard->PossibleHiddenSearches, 0, "all searches are visible"); + +$m->no_warnings_ok; + +$m->get_ok("/Dashboards/Modify.html?id=$id"); +$m->content_lacks("inner dashboard", "no SeeGroupDashboard right"); +$m->content_contains("Permission denied"); + +$m->warning_like(qr/Permission denied/, "got a permission denied warning"); + +$user_obj->PrincipalObj->GrantRight(Right => 'SeeGroupDashboard', Object => $inner_group); +$m->get_ok("/Dashboards/Modify.html?id=$id"); +$m->content_contains("inner dashboard", "we now have SeeGroupDashboard right"); +$m->content_lacks("Permission denied"); + +$m->content_contains('Subscription', "Subscription link not hidden because we have SubscribeDashboard"); + diff --git a/rt/t/web/dashboards-permissions.t b/rt/t/web/dashboards-permissions.t new file mode 100644 index 000000000..172404289 --- /dev/null +++ b/rt/t/web/dashboards-permissions.t @@ -0,0 +1,38 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => 7; +my ($baseurl, $m) = RT::Test->started_ok; + +my $url = $m->rt_base_url; + +# create user and queue {{{ +my $user_obj = RT::User->new($RT::SystemUser); +my ($ok, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com'); +ok($ok, 'ACL test user creation'); +$user_obj->SetName('customer'); +$user_obj->SetPrivileged(1); +($ok, $msg) = $user_obj->SetPassword('customer'); +$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf'); +my $currentuser = RT::CurrentUser->new($user_obj); + +my $queue = RT::Queue->new($RT::SystemUser); +$queue->Create(Name => 'SearchQueue'.$$); + +$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $queue) + for qw/SeeQueue ShowTicket OwnTicket/; + +$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $RT::System) + for qw/SubscribeDashboard CreateOwnDashboard SeeOwnDashboard ModifyOwnDashboard DeleteOwnDashboard/; +# }}} + +ok $m->login(customer => 'customer'), "logged in"; + +$m->get_ok("$url/Dashboards"); + +$m->follow_link_ok({text => "New"}); +$m->form_name('ModifyDashboard'); +is_deeply([$m->current_form->find_input('Privacy')->possible_values], ["RT::User-" . $user_obj->Id], "the only selectable privacy is user"); +$m->content_lacks('Delete', "Delete button hidden because we are creating"); + diff --git a/rt/t/web/dashboards.t b/rt/t/web/dashboards.t new file mode 100644 index 000000000..9d98ce6e4 --- /dev/null +++ b/rt/t/web/dashboards.t @@ -0,0 +1,250 @@ +#!/usr/bin/perl -w +use strict; + +use RT::Test tests => 109; +my ($baseurl, $m) = RT::Test->started_ok; + +my $url = $m->rt_base_url; + +my $user_obj = RT::User->new($RT::SystemUser); +my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com'); +ok($ret, 'ACL test user creation'); +$user_obj->SetName('customer'); +$user_obj->SetPrivileged(1); +($ret, $msg) = $user_obj->SetPassword('customer'); +$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf'); +my $currentuser = RT::CurrentUser->new($user_obj); + +my $onlooker = RT::User->new($RT::SystemUser); +($ret, $msg) = $onlooker->LoadOrCreateByEmail('onlooker@example.com'); +ok($ret, 'ACL test user creation'); +$onlooker->SetName('onlooker'); +$onlooker->SetPrivileged(1); +($ret, $msg) = $onlooker->SetPassword('onlooker'); + +my $queue = RT::Queue->new($RT::SystemUser); +$queue->Create(Name => 'SearchQueue'.$$); + +for my $user ($user_obj, $onlooker) { + $user->PrincipalObj->GrantRight(Right => 'ModifySelf'); + for my $right (qw/SeeQueue ShowTicket OwnTicket/) { + $user->PrincipalObj->GrantRight(Right => $right, Object => $queue); + } +} + +ok $m->login(customer => 'customer'), "logged in"; + +$m->get_ok($url."Dashboards/index.html"); +$m->content_lacks('<a href="/Dashboards/Modify.html?Create=1">New</a>', + "No 'new dashboard' link because we have no CreateOwnDashboard"); + +$m->no_warnings_ok; + +$m->get_ok($url."Dashboards/Modify.html?Create=1"); +$m->content_contains("Permission denied"); +$m->content_lacks("Save Changes"); + +$m->warning_like(qr/Permission denied/, "got a permission denied warning"); + +$user_obj->PrincipalObj->GrantRight(Right => 'ModifyOwnDashboard', Object => $RT::System); + +# Modify itself is no longer good enough, you need Create +$m->get_ok($url."Dashboards/Modify.html?Create=1"); +$m->content_contains("Permission denied"); +$m->content_lacks("Save Changes"); + +$m->warning_like(qr/Permission denied/, "got a permission denied warning"); + +$user_obj->PrincipalObj->GrantRight(Right => 'CreateOwnDashboard', Object => $RT::System); + +$m->get_ok($url."Dashboards/Modify.html?Create=1"); +$m->content_lacks("Permission denied"); +$m->content_contains("Create"); + +$m->get_ok($url."Dashboards/index.html"); +$m->content_contains("New", "'New' link because we now have ModifyOwnDashboard"); + +$m->follow_link_ok({text => "New"}); +$m->form_name('ModifyDashboard'); +$m->field("Name" => 'different dashboard'); +$m->content_lacks('Delete', "Delete button hidden because we are creating"); +$m->click_button(value => 'Create'); +$m->content_lacks("No permission to create dashboards"); +$m->content_contains("Saved dashboard different dashboard"); +$m->content_lacks('Delete', "Delete button hidden because we lack DeleteOwnDashboard"); + +$m->get_ok($url."Dashboards/index.html"); +$m->content_lacks("different dashboard", "we lack SeeOwnDashboard"); + +$user_obj->PrincipalObj->GrantRight(Right => 'SeeOwnDashboard', Object => $RT::System); + +$m->get_ok($url."Dashboards/index.html"); +$m->content_contains("different dashboard", "we now have SeeOwnDashboard"); +$m->content_lacks("Permission denied"); + +$m->follow_link_ok({text => "different dashboard"}); +$m->content_contains("Basics"); +$m->content_contains("Queries"); +$m->content_lacks("Subscription", "we don't have the SubscribeDashboard right"); + +$m->follow_link_ok({text => "Basics"}); +$m->content_contains("Modify the dashboard different dashboard"); + +$m->follow_link_ok({text => "Queries"}); +$m->content_contains("Modify the queries of dashboard different dashboard"); +$m->form_name('Dashboard-Searches-body'); +$m->field('Searches-body-Available' => ["search-2-RT::System-1"]); +$m->click_button(name => 'add'); +$m->content_contains("Dashboard updated"); + +my $dashboard = RT::Dashboard->new($currentuser); +my ($id) = $m->content =~ /name="id" value="(\d+)"/; +ok($id, "got an ID, $id"); +$dashboard->LoadById($id); +is($dashboard->Name, "different dashboard"); + +is($dashboard->Privacy, 'RT::User-' . $user_obj->Id, "correct privacy"); +is($dashboard->PossibleHiddenSearches, 0, "all searches are visible"); + +my @searches = $dashboard->Searches; +is(@searches, 1, "one saved search in the dashboard"); +like($searches[0]->Name, qr/newest unowned tickets/, "correct search name"); + +$m->form_name('Dashboard-Searches-body'); +$m->field('Searches-body-Available' => ["search-1-RT::System-1"]); +$m->click_button(name => 'add'); +$m->content_contains("Dashboard updated"); + +RT::Record->FlushCache if RT::Record->can('FlushCache'); +$dashboard = RT::Dashboard->new($currentuser); +$dashboard->LoadById($id); + +@searches = $dashboard->Searches; +is(@searches, 2, "two saved searches in the dashboard"); +like($searches[0]->Name, qr/newest unowned tickets/, "correct existing search name"); +like($searches[1]->Name, qr/highest priority tickets I own/, "correct new search name"); + +my $ticket = RT::Ticket->new($RT::SystemUser); +$ticket->Create( + Queue => $queue->Id, + Requestor => [ $user_obj->Name ], + Owner => $user_obj, + Subject => 'dashboard test', +); + +$m->follow_link_ok({text => 'different dashboard'}); +$m->content_contains("20 highest priority tickets I own"); +$m->content_contains("20 newest unowned tickets"); +$m->content_lacks("Bookmarked Tickets"); +$m->content_contains("dashboard test", "ticket subject"); + +$m->get_ok("/Dashboards/$id/This fragment left intentionally blank"); +$m->content_contains("20 highest priority tickets I own"); +$m->content_contains("20 newest unowned tickets"); +$m->content_lacks("Bookmarked Tickets"); +$m->content_contains("dashboard test", "ticket subject"); + +$m->get_ok("/Dashboards/Subscription.html?DashboardId=$id"); +$m->form_name('SubscribeDashboard'); +$m->click_button(name => 'Save'); +$m->content_contains("Permission denied"); +$m->warning_like(qr/Unable to subscribe to dashboard.*Permission denied/, "got a permission denied warning when trying to subscribe to a dashboard"); + +RT::Record->FlushCache if RT::Record->can('FlushCache'); +is($user_obj->Attributes->Named('Subscription'), 0, "no subscriptions"); + +$user_obj->PrincipalObj->GrantRight(Right => 'SubscribeDashboard', Object => $RT::System); + +$m->get_ok("/Dashboards/Modify.html?id=$id"); +$m->follow_link_ok({text => "Subscription"}); +$m->content_contains("Subscribe to dashboard different dashboard"); +$m->content_contains("Unowned Tickets"); +$m->content_contains("My Tickets"); +$m->content_lacks("Bookmarked Tickets", "only dashboard queries show up"); + +$m->form_name('SubscribeDashboard'); +$m->click_button(name => 'Save'); +$m->content_lacks("Permission denied"); +$m->content_contains("Subscribed to dashboard different dashboard"); + +RT::Record->FlushCache if RT::Record->can('FlushCache'); +TODO: { + local $TODO = "some kind of caching is still happening (it works if I remove the check above)"; + is($user_obj->Attributes->Named('Subscription'), 1, "we have a subscription"); +}; + +$m->get_ok("/Dashboards/Modify.html?id=$id"); +$m->follow_link_ok({text => "Subscription"}); +$m->content_contains("Modify the subscription to dashboard different dashboard"); + +$m->get_ok("/Dashboards/Modify.html?id=$id&Delete=1"); +$m->content_contains("Permission denied", "unable to delete dashboard because we lack DeleteOwnDashboard"); + +$m->warning_like(qr/Couldn't delete dashboard.*Permission denied/, "got a permission denied warning when trying to delete the dashboard"); + +$user_obj->PrincipalObj->GrantRight(Right => 'DeleteOwnDashboard', Object => $RT::System); + +$m->get_ok("/Dashboards/Modify.html?id=$id"); +$m->content_contains('Delete', "Delete button shows because we have DeleteOwnDashboard"); + +$m->form_name('ModifyDashboard'); +$m->click_button(name => 'Delete'); +$m->content_contains("Deleted dashboard $id"); + +$m->get("/Dashboards/Modify.html?id=$id"); +$m->content_lacks("different dashboard", "dashboard was deleted"); +$m->content_contains("Failed to load dashboard $id"); + +$m->warning_like(qr/Failed to load dashboard.*Couldn't find row/, "the dashboard was deleted"); + +$user_obj->PrincipalObj->GrantRight(Right => "SuperUser", Object => $RT::System); + +# now test that we warn about searches others can't see +# first create a personal saved search... +$m->get_ok($url."Search/Build.html"); +$m->follow_link_ok({text => 'Advanced'}); +$m->form_with_fields('Query'); +$m->field(Query => "id > 0"); +$m->submit; + +$m->form_with_fields('SavedSearchDescription'); +$m->field(SavedSearchDescription => "personal search"); +$m->click_button(name => "SavedSearchSave"); + +# then the system-wide dashboard +$m->get_ok($url."Dashboards/Modify.html?Create=1"); + +$m->form_name('ModifyDashboard'); +$m->field("Name" => 'system dashboard'); +$m->field("Privacy" => 'RT::System-1'); +$m->content_lacks('Delete', "Delete button hidden because we are creating"); +$m->click_button(value => 'Create'); +$m->content_lacks("No permission to create dashboards"); +$m->content_contains("Saved dashboard system dashboard"); + +$m->follow_link_ok({text => 'Queries'}); + +$m->form_name('Dashboard-Searches-body'); +$m->field('Searches-body-Available' => ['search-7-RT::User-22']); # XXX: :( :( +$m->click_button(name => 'add'); +$m->content_contains("Dashboard updated"); + +$m->content_contains("The following queries may not be visible to all users who can see this dashboard."); + +$m->follow_link_ok({text => 'system dashboard'}); +$m->content_contains("personal search", "saved search shows up"); +$m->content_contains("dashboard test", "matched ticket shows up"); + +# make sure the onlooker can't see the search... +$onlooker->PrincipalObj->GrantRight(Right => 'SeeDashboard', Object => $RT::System); + +my $omech = RT::Test::Web->new; +ok $omech->login(onlooker => 'onlooker'), "logged in"; +$omech->get_ok("/Dashboards"); + +$omech->follow_link_ok({text => 'system dashboard'}); +$omech->content_lacks("personal search", "saved search doesn't show up"); +$omech->content_lacks("dashboard test", "matched ticket doesn't show up"); + +$m->warning_like(qr/User .* tried to load container user /, "can't see other users' personal searches"); + diff --git a/rt/t/web/gnupg-outgoing.t b/rt/t/web/gnupg-outgoing.t new file mode 100644 index 000000000..a46833c6c --- /dev/null +++ b/rt/t/web/gnupg-outgoing.t @@ -0,0 +1,363 @@ +#!/usr/bin/perl -w +use strict; +use warnings; + +use RT::Test tests => 492; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use RT::Action::SendEmail; +use File::Temp qw(tempdir); + +RT::Test->set_mail_catcher; + +use_ok('RT::Crypt::GnuPG'); + +RT->Config->Set( GnuPG => + Enable => 1, + OutgoingMessagesFormat => 'RFC', +); + +RT->Config->Set( GnuPGOptions => + homedir => scalar tempdir( CLEANUP => 1 ), + passphrase => 'rt-test', + 'no-permission-warning' => undef, + 'trust-model' => 'always', +); +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + +RT::Test->import_gnupg_key('rt-recipient@example.com'); +RT::Test->import_gnupg_key('rt-test@example.com', 'public'); + +my $queue = RT::Test->load_or_create_queue( + Name => 'Regression', + CorrespondAddress => 'rt-recipient@example.com', + CommentAddress => 'rt-recipient@example.com', +); +ok $queue && $queue->id, 'loaded or created queue'; + +RT::Test->set_rights( + Principal => 'Everyone', + Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'], +); + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + +my @variants = ( + {}, + { Sign => 1 }, + { Encrypt => 1 }, + { Sign => 1, Encrypt => 1 }, +); + +# collect emails +my %mail = ( + plain => [], + signed => [], + encrypted => [], + signed_encrypted => [], +); + +diag "check in read-only mode that queue's props influence create/update ticket pages" if $ENV{TEST_VERBOSE}; +{ + foreach my $variant ( @variants ) { + set_queue_crypt_options( %$variant ); + $m->goto_create_ticket( $queue ); + $m->form_name('TicketCreate'); + if ( $variant->{'Encrypt'} ) { + ok $m->value('Encrypt', 2), "encrypt tick box is checked"; + } else { + ok !$m->value('Encrypt', 2), "encrypt tick box is unchecked"; + } + if ( $variant->{'Sign'} ) { + ok $m->value('Sign', 2), "sign tick box is checked"; + } else { + ok !$m->value('Sign', 2), "sign tick box is unchecked"; + } + } + + # to avoid encryption/signing during create + set_queue_crypt_options(); + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + my ($id) = $ticket->Create( + Subject => 'test', + Queue => $queue->id, + Requestor => 'rt-test@example.com', + ); + ok $id, 'ticket created'; + + foreach my $variant ( @variants ) { + set_queue_crypt_options( %$variant ); + $m->goto_ticket( $id ); + $m->follow_link_ok({text => 'Reply'}, '-> reply'); + $m->form_number(3); + if ( $variant->{'Encrypt'} ) { + ok $m->value('Encrypt', 2), "encrypt tick box is checked"; + } else { + ok !$m->value('Encrypt', 2), "encrypt tick box is unchecked"; + } + if ( $variant->{'Sign'} ) { + ok $m->value('Sign', 2), "sign tick box is checked"; + } else { + ok !$m->value('Sign', 2), "sign tick box is unchecked"; + } + } +} + +# create a ticket for each combination +foreach my $queue_set ( @variants ) { + set_queue_crypt_options( %$queue_set ); + foreach my $ticket_set ( @variants ) { + create_a_ticket( %$ticket_set ); + } +} + +my $tid; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + ($tid) = $ticket->Create( + Subject => 'test', + Queue => $queue->id, + Requestor => 'rt-test@example.com', + ); + ok $tid, 'ticket created'; +} + +# again for each combination add a reply message +foreach my $queue_set ( @variants ) { + set_queue_crypt_options( %$queue_set ); + foreach my $ticket_set ( @variants ) { + update_ticket( $tid, %$ticket_set ); + } +} + + +# ------------------------------------------------------------------------------ +# now delete all keys from the keyring and put back secret/pub pair for rt-test@ +# and only public key for rt-recipient@ so we can verify signatures and decrypt +# like we are on another side recieve emails +# ------------------------------------------------------------------------------ + +unlink $_ foreach glob( RT->Config->Get('GnuPGOptions')->{'homedir'} ."/*" ); +RT::Test->import_gnupg_key('rt-recipient@example.com', 'public'); +RT::Test->import_gnupg_key('rt-test@example.com'); + +$queue = RT::Test->load_or_create_queue( + Name => 'Regression', + CorrespondAddress => 'rt-test@example.com', + CommentAddress => 'rt-test@example.com', +); +ok $queue && $queue->id, 'changed props of the queue'; + +foreach my $mail ( map cleanup_headers($_), @{ $mail{'plain'} } ) { + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + ok !$msg->GetHeader('X-RT-Privacy'), "RT's outgoing mail has no crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted', + "RT's outgoing mail looks not encrypted"; + ok !$msg->GetHeader('X-RT-Incoming-Signature'), + "RT's outgoing mail looks not signed"; + + like $msg->Content, qr/Some content/, "RT's mail includes copy of ticket text"; +} + +foreach my $mail ( map cleanup_headers($_), @{ $mail{'signed'} } ) { + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is $msg->GetHeader('X-RT-Privacy'), 'PGP', + "RT's outgoing mail has crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted', + "RT's outgoing mail looks not encrypted"; + like $msg->GetHeader('X-RT-Incoming-Signature'), + qr/<rt-recipient\@example.com>/, + "RT's outgoing mail looks signed"; + + like $attachments[0]->Content, qr/Some content/, + "RT's mail includes copy of ticket text"; +} + +foreach my $mail ( map cleanup_headers($_), @{ $mail{'encrypted'} } ) { + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is $msg->GetHeader('X-RT-Privacy'), 'PGP', + "RT's outgoing mail has crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success', + "RT's outgoing mail looks encrypted"; + ok !$msg->GetHeader('X-RT-Incoming-Signature'), + "RT's outgoing mail looks not signed"; + + like $attachments[0]->Content, qr/Some content/, + "RT's mail includes copy of ticket text"; +} + +foreach my $mail ( map cleanup_headers($_), @{ $mail{'signed_encrypted'} } ) { + my ($status, $id) = RT::Test->send_via_mailgate($mail); + is ($status >> 8, 0, "The mail gateway exited normally"); + ok ($id, "got id of a newly created ticket - $id"); + + my $tick = RT::Ticket->new( $RT::SystemUser ); + $tick->Load( $id ); + ok ($tick->id, "loaded ticket #$id"); + + my $txn = $tick->Transactions->First; + my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef}; + + is $msg->GetHeader('X-RT-Privacy'), 'PGP', + "RT's outgoing mail has crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success', + "RT's outgoing mail looks encrypted"; + like $msg->GetHeader('X-RT-Incoming-Signature'), + qr/<rt-recipient\@example.com>/, + "RT's outgoing mail looks signed"; + + like $attachments[0]->Content, qr/Some content/, + "RT's mail includes copy of ticket text"; +} + +sub create_a_ticket { + my %args = (@_); + + RT::Test->clean_caught_mails; + + $m->goto_create_ticket( $queue ); + $m->form_name('TicketCreate'); + $m->field( Subject => 'test' ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + + foreach ( qw(Sign Encrypt) ) { + if ( $args{ $_ } ) { + $m->tick( $_ => 1 ); + } else { + $m->untick( $_ => 1 ); + } + } + + $m->submit; + is $m->status, 200, "request successful"; + + unlike($m->content, qr/unable to sign outgoing email messages/); + + $m->get_ok('/'); # ensure that the mail has been processed + + my @mail = RT::Test->fetch_caught_mails; + check_text_emails( \%args, @mail ); +} + +sub update_ticket { + my $tid = shift; + my %args = (@_); + + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->field( UpdateContent => 'Some content' ); + + foreach ( qw(Sign Encrypt) ) { + if ( $args{ $_ } ) { + $m->tick( $_ => 1 ); + } else { + $m->untick( $_ => 1 ); + } + } + + $m->click('SubmitTicket'); + is $m->status, 200, "request successful"; + $m->content_like(qr/Message recorded/, 'Message recorded') or diag $m->content; + + $m->get_ok('/'); # ensure that the mail has been processed + + my @mail = RT::Test->fetch_caught_mails; + check_text_emails( \%args, @mail ); +} + +sub check_text_emails { + my %args = %{ shift @_ }; + my @mail = @_; + + ok scalar @mail, "got some mail"; + for my $mail (@mail) { + if ( $args{'Encrypt'} ) { + unlike $mail, qr/Some content/, "outgoing email was encrypted"; + } else { + like $mail, qr/Some content/, "outgoing email was not encrypted"; + } + if ( $args{'Sign'} && $args{'Encrypt'} ) { + like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed'; + } elsif ( $args{'Sign'} ) { + like $mail, qr/SIGNATURE/, 'outgoing email was signed'; + } else { + unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed'; + } + } + if ( $args{'Sign'} && $args{'Encrypt'} ) { + push @{ $mail{'signed_encrypted'} }, @mail; + } elsif ( $args{'Sign'} ) { + push @{ $mail{'signed'} }, @mail; + } elsif ( $args{'Encrypt'} ) { + push @{ $mail{'encrypted'} }, @mail; + } else { + push @{ $mail{'plain'} }, @mail; + } +} + +sub cleanup_headers { + my $mail = shift; + # strip id from subject to create new ticket + $mail =~ s/^(Subject:)\s*\[.*?\s+#\d+\]\s*/$1 /m; + # strip several headers + foreach my $field ( qw(Message-ID X-RT-Original-Encoding RT-Originator RT-Ticket X-RT-Loop-Prevention) ) { + $mail =~ s/^$field:.*?\n(?! |\t)//gmsi; + } + return $mail; +} + +sub set_queue_crypt_options { + my %args = @_; + $m->get_ok("/Admin/Queues/Modify.html?id=". $queue->id); + $m->form_with_fields('Sign', 'Encrypt'); + foreach my $opt ('Sign', 'Encrypt') { + if ( $args{$opt} ) { + $m->tick($opt => 1); + } else { + $m->untick($opt => 1); + } + } + $m->submit; +} + diff --git a/rt/t/web/gnupg-select-keys-on-create.t b/rt/t/web/gnupg-select-keys-on-create.t new file mode 100644 index 000000000..deee6b291 --- /dev/null +++ b/rt/t/web/gnupg-select-keys-on-create.t @@ -0,0 +1,325 @@ +#!/usr/bin/perl -w +use strict; +use warnings; + +use RT::Test tests => 60; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use RT::Action::SendEmail; +use File::Temp qw(tempdir); + +RT::Test->set_mail_catcher; + +use_ok('RT::Crypt::GnuPG'); + +RT->Config->Set( GnuPG => + Enable => 1, + OutgoingMessagesFormat => 'RFC', +); + +RT->Config->Set( GnuPGOptions => + homedir => scalar tempdir( CLEANUP => 0 ), + passphrase => 'rt-test', + 'no-permission-warning' => undef, +); +diag "GnuPG --homedir ". RT->Config->Get('GnuPGOptions')->{'homedir'} if $ENV{TEST_VERBOSE}; + +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + +my $queue = RT::Test->load_or_create_queue( + Name => 'Regression', + CorrespondAddress => 'rt-recipient@example.com', + CommentAddress => 'rt-recipient@example.com', +); +ok $queue && $queue->id, 'loaded or created queue'; + +RT::Test->set_rights( + Principal => 'Everyone', + Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'], +); + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + +diag "check that signing doesn't work if there is no key" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_create_ticket( $queue ), "UI -> create ticket"; + $m->form_number(3); + $m->tick( Sign => 1 ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + $m->submit; + $m->content_like( + qr/unable to sign outgoing email messages/i, + 'problems with passphrase' + ); + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +{ + RT::Test->import_gnupg_key('rt-recipient@example.com'); + RT::Test->trust_gnupg_key('rt-recipient@example.com'); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-recipient@example.com'); + is $res{'info'}[0]{'TrustTerse'}, 'ultimate', 'ultimately trusted key'; +} + +diag "check that things don't work if there is no key" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_create_ticket( $queue ), "UI -> create ticket"; + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There is no key suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok !$form->find_input( 'UseKey-rt-test@example.com' ), 'no key selector'; + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +diag "import first key of rt-test\@example.com" if $ENV{TEST_VERBOSE}; +my $fpr1 = ''; +{ + RT::Test->import_gnupg_key('rt-test@example.com', 'public'); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com'); + is $res{'info'}[0]{'TrustLevel'}, 0, 'is not trusted key'; + $fpr1 = $res{'info'}[0]{'Fingerprint'}; +} + +diag "check that things still doesn't work if key is not trusted" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_create_ticket( $queue ), "UI -> create ticket"; + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There is one suitable key, but trust level is not set/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 1, 'one option'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/Selected key either is not trusted/i, + 'problems with keys' + ); + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +diag "import a second key of rt-test\@example.com" if $ENV{TEST_VERBOSE}; +my $fpr2 = ''; +{ + RT::Test->import_gnupg_key('rt-test@example.com.2', 'public'); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com'); + is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key'; + $fpr2 = $res{'info'}[2]{'Fingerprint'}; +} + +diag "check that things still doesn't work if two keys are not trusted" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_create_ticket( $queue ), "UI -> create ticket"; + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/Selected key either is not trusted/i, + 'problems with keys' + ); + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +{ + RT::Test->lsign_gnupg_key( $fpr1 ); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com'); + ok $res{'info'}[0]{'TrustLevel'} > 0, 'trusted key'; + is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key'; +} + +diag "check that we see key selector even if only one key is trusted but there are more keys"; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_create_ticket( $queue ), "UI -> create ticket"; + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +diag "check that key selector works and we can select trusted key"; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_create_ticket( $queue ), "UI -> create ticket"; + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->submit; + $m->content_like( qr/Ticket \d+ created in queue/i, 'ticket created' ); + + my @mail = RT::Test->fetch_caught_mails; + ok @mail, 'there are some emails'; + check_text_emails( { Encrypt => 1 }, @mail ); +} + +diag "check encrypting of attachments"; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_create_ticket( $queue ), "UI -> create ticket"; + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + $m->field( Attach => $0 ); + $m->submit; + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->submit; + $m->content_like( qr/Ticket \d+ created in queue/i, 'ticket created' ); + + my @mail = RT::Test->fetch_caught_mails; + ok @mail, 'there are some emails'; + check_text_emails( { Encrypt => 1, Attachment => 1 }, @mail ); +} + +sub check_text_emails { + my %args = %{ shift @_ }; + my @mail = @_; + + ok scalar @mail, "got some mail"; + for my $mail (@mail) { + for my $type ('email', 'attachment') { + next if $type eq 'attachment' && !$args{'Attachment'}; + + my $content = $type eq 'email' + ? "Some content" + : "Attachment content"; + + if ( $args{'Encrypt'} ) { + unlike $mail, qr/$content/, "outgoing $type was encrypted"; + } else { + like $mail, qr/$content/, "outgoing $type was not encrypted"; + } + + next unless $type eq 'email'; + + if ( $args{'Sign'} && $args{'Encrypt'} ) { + like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed'; + } elsif ( $args{'Sign'} ) { + like $mail, qr/SIGNATURE/, 'outgoing email was signed'; + } else { + unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed'; + } + } + } +} + diff --git a/rt/t/web/gnupg-select-keys-on-update.t b/rt/t/web/gnupg-select-keys-on-update.t new file mode 100644 index 000000000..76817ddf2 --- /dev/null +++ b/rt/t/web/gnupg-select-keys-on-update.t @@ -0,0 +1,344 @@ +#!/usr/bin/perl -w +use strict; +use warnings; + +use RT::Test tests => 68; + +plan skip_all => 'GnuPG required.' + unless eval 'use GnuPG::Interface; 1'; +plan skip_all => 'gpg executable is required.' + unless RT::Test->find_executable('gpg'); + + +use RT::Action::SendEmail; +use File::Temp qw(tempdir); + +RT::Test->set_mail_catcher; + +use_ok('RT::Crypt::GnuPG'); + +RT->Config->Set( GnuPG => + Enable => 1, + OutgoingMessagesFormat => 'RFC', +); + +RT->Config->Set( GnuPGOptions => + homedir => scalar tempdir( CLEANUP => 0 ), + passphrase => 'rt-test', + 'no-permission-warning' => undef, +); +diag "GnuPG --homedir ". RT->Config->Get('GnuPGOptions')->{'homedir'} if $ENV{TEST_VERBOSE}; + +RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' ); + +my $queue = RT::Test->load_or_create_queue( + Name => 'Regression', + CorrespondAddress => 'rt-recipient@example.com', + CommentAddress => 'rt-recipient@example.com', +); +ok $queue && $queue->id, 'loaded or created queue'; + +RT::Test->set_rights( + Principal => 'Everyone', + Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'], +); + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + + +my $tid; +{ + my $ticket = RT::Ticket->new( $RT::SystemUser ); + ($tid) = $ticket->Create( + Subject => 'test', + Queue => $queue->id, + ); + ok $tid, 'ticket created'; +} + +diag "check that signing doesn't work if there is no key" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->tick( Sign => 1 ); + $m->field( UpdateCc => 'rt-test@example.com' ); + $m->field( UpdateContent => 'Some content' ); + $m->click('SubmitTicket'); + $m->content_like( + qr/unable to sign outgoing email messages/i, + 'problems with passphrase' + ); + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +{ + RT::Test->import_gnupg_key('rt-recipient@example.com'); + RT::Test->trust_gnupg_key('rt-recipient@example.com'); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-recipient@example.com'); + is $res{'info'}[0]{'TrustTerse'}, 'ultimate', 'ultimately trusted key'; +} + +diag "check that things don't work if there is no key" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( UpdateCc => 'rt-test@example.com' ); + $m->field( UpdateContent => 'Some content' ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There is no key suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok !$form->find_input( 'UseKey-rt-test@example.com' ), 'no key selector'; + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + + +diag "import first key of rt-test\@example.com" if $ENV{TEST_VERBOSE}; +my $fpr1 = ''; +{ + RT::Test->import_gnupg_key('rt-test@example.com', 'public'); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com'); + is $res{'info'}[0]{'TrustLevel'}, 0, 'is not trusted key'; + $fpr1 = $res{'info'}[0]{'Fingerprint'}; +} + +diag "check that things still doesn't work if key is not trusted" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( UpdateCc => 'rt-test@example.com' ); + $m->field( UpdateContent => 'Some content' ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There is one suitable key, but trust level is not set/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 1, 'one option'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/Selected key either is not trusted/i, + 'problems with keys' + ); + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +diag "import a second key of rt-test\@example.com" if $ENV{TEST_VERBOSE}; +my $fpr2 = ''; +{ + RT::Test->import_gnupg_key('rt-test@example.com.2', 'public'); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com'); + is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key'; + $fpr2 = $res{'info'}[2]{'Fingerprint'}; +} + +diag "check that things still doesn't work if two keys are not trusted" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( UpdateCc => 'rt-test@example.com' ); + $m->field( UpdateContent => 'Some content' ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/Selected key either is not trusted/i, + 'problems with keys' + ); + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +{ + RT::Test->lsign_gnupg_key( $fpr1 ); + my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com'); + ok $res{'info'}[0]{'TrustLevel'} > 0, 'trusted key'; + is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key'; +} + +diag "check that we see key selector even if only one key is trusted but there are more keys" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( UpdateCc => 'rt-test@example.com' ); + $m->field( UpdateContent => 'Some content' ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + my @mail = RT::Test->fetch_caught_mails; + ok !@mail, 'there are no outgoing emails'; +} + +diag "check that key selector works and we can select trusted key" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( UpdateCc => 'rt-test@example.com' ); + $m->field( UpdateContent => 'Some content' ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->click('SubmitTicket'); + $m->content_like( qr/Message recorded/i, 'Message recorded' ); + + my @mail = RT::Test->fetch_caught_mails; + ok @mail, 'there are some emails'; + check_text_emails( { Encrypt => 1 }, @mail ); +} + +diag "check encrypting of attachments" if $ENV{TEST_VERBOSE}; +{ + RT::Test->clean_caught_mails; + + ok $m->goto_ticket( $tid ), "UI -> ticket #$tid"; + $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' ); + $m->form_number(3); + $m->tick( Encrypt => 1 ); + $m->field( UpdateCc => 'rt-test@example.com' ); + $m->field( UpdateContent => 'Some content' ); + $m->field( Attach => $0 ); + $m->click('SubmitTicket'); + $m->content_like( + qr/You are going to encrypt outgoing email messages/i, + 'problems with keys' + ); + $m->content_like( + qr/There are several keys suitable for encryption/i, + 'problems with keys' + ); + + my $form = $m->form_number(3); + ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector'; + is scalar $input->possible_values, 2, 'two options'; + + $m->select( 'UseKey-rt-test@example.com' => $fpr1 ); + $m->click('SubmitTicket'); + $m->content_like( qr/Message recorded/i, 'Message recorded' ); + + my @mail = RT::Test->fetch_caught_mails; + ok @mail, 'there are some emails'; + check_text_emails( { Encrypt => 1, Attachment => 1 }, @mail ); +} + +sub check_text_emails { + my %args = %{ shift @_ }; + my @mail = @_; + + ok scalar @mail, "got some mail"; + for my $mail (@mail) { + for my $type ('email', 'attachment') { + next if $type eq 'attachment' && !$args{'Attachment'}; + + my $content = $type eq 'email' + ? "Some content" + : "Attachment content"; + + if ( $args{'Encrypt'} ) { + unlike $mail, qr/$content/, "outgoing $type was encrypted"; + } else { + like $mail, qr/$content/, "outgoing $type was not encrypted"; + } + + next unless $type eq 'email'; + + if ( $args{'Sign'} && $args{'Encrypt'} ) { + like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed'; + } elsif ( $args{'Sign'} ) { + like $mail, qr/SIGNATURE/, 'outgoing email was signed'; + } else { + unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed'; + } + } + } +} + diff --git a/rt/t/web/offline_messages_utf8.t b/rt/t/web/offline_messages_utf8.t new file mode 100644 index 000000000..c32e0bc27 --- /dev/null +++ b/rt/t/web/offline_messages_utf8.t @@ -0,0 +1,67 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 6; +use File::Temp qw/tempfile/; +use Encode; +use RT::Ticket; + +my ( $url, $m ) = RT::Test->started_ok; +$m->default_header( 'Accept-Language' => "zh-cn" ); +ok( $m->login, 'logged in' ); + +my $ticket_id; +my $template; + +{ + + # test create message + $template = <<EOF; +===Create-Ticket: ticket1 +Queue: General +Subject: test message +Status: new +Content: +ENDOFCONTENT +Due: +TimeEstimated: 100 +TimeLeft: 100 +FinalPriority: 90 +EOF + + $m->get_ok( $url . '/Tools/Offline.html' ); + + $m->submit_form( + form_name => 'TicketUpdate', + fields => { string => $template, }, + button => 'UpdateTickets', + ); + my $content = encode 'utf8', $m->content; + ok( $content =~ qr/ç”³è¯·å• #(\d+) æˆåŠŸæ–°å¢žäºŽ 'General' 表å•/, 'message is shown right' ); + $ticket_id = $1; +} + +{ + + # test update message + $template = <<EOF; +===Update-Ticket: 1 +Subject: test message update +EOF + + $m->get_ok( $url . '/Tools/Offline.html' ); + $m->submit_form( + form_name => 'TicketUpdate', + fields => { string => $template, }, + button => 'UpdateTickets', + ); + + my $content = encode 'utf8', $m->content; + ok( + $content =~ +qr/主题\s*的值从\s*'test message'\s*改为\s*'test message update'/, + 'subject is updated' + ); +} + diff --git a/rt/t/web/offline_utf8.t b/rt/t/web/offline_utf8.t new file mode 100644 index 000000000..2a3e64d3c --- /dev/null +++ b/rt/t/web/offline_utf8.t @@ -0,0 +1,54 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 8; +use File::Temp qw/tempfile/; +use Encode; +use RT::Ticket; +my ( $fh, $file ) = tempfile; +my $template = <<EOF; +===Create-Ticket: ticket1 +Queue: General +Subject: æ ‡é¢˜ +Status: new +Content: +这是æ£æ–‡ +ENDOFCONTENT +EOF + +print $fh $template; +close $fh; + +my ( $url, $m ) = RT::Test->started_ok; +ok( $m->login, 'logged in' ); + +$m->get_ok( $url . '/Tools/Offline.html' ); + +$m->submit_form( + form_name => 'TicketUpdate', + fields => { Template => $file, }, + button => 'Parse', +); + +$m->content_contains( '这是æ£æ–‡', 'content is parsed right' ); + +$m->submit_form( + form_name => 'TicketUpdate', + button => 'UpdateTickets', + + # mimic what browsers do: they seems decoded $template + fields => { string => decode( 'utf8', $template ), }, +); + +$m->content_like( qr/Ticket \d+ created/, 'found ticket created message' ); +my ( $ticket_id ) = $m->content =~ /Ticket (\d+) created/; + +my $ticket = RT::Ticket->new( $RT::SystemUser ); +$ticket->Load( $ticket_id ); +is( $ticket->Subject, 'æ ‡é¢˜', 'subject in $ticket is right' ); + +$m->get_ok( $url . "/Ticket/Display.html?id=$ticket_id" ); +$m->content_contains( '这是æ£æ–‡', + 'content is right in ticket display page' ); + diff --git a/rt/t/web/query_builder.t b/rt/t/web/query_builder.t new file mode 100644 index 000000000..02ed1297f --- /dev/null +++ b/rt/t/web/query_builder.t @@ -0,0 +1,249 @@ +#!/usr/bin/perl + +use strict; +use HTTP::Request::Common; +use HTTP::Cookies; +use LWP; +use Encode; +use RT::Test tests => 42; + +my $cookie_jar = HTTP::Cookies->new; +my ($baseurl, $agent) = RT::Test->started_ok; + + +# give the agent a place to stash the cookies + +$agent->cookie_jar($cookie_jar); + +# create a regression queue if it doesn't exist +my $queue = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $queue && $queue->id, 'loaded or created queue'; + +my $url = $agent->rt_base_url; +ok $agent->login, "logged in"; + +# {{{ Query Builder tests + +my $response = $agent->get($url."Search/Build.html"); +ok $response->is_success, "Fetched ". $url ."Search/Build.html"; + +sub getQueryFromForm { + $agent->form_name('BuildQuery'); + # This pulls out the "hidden input" query from the page + my $q = $agent->current_form->find_input("Query")->value; + $q =~ s/^\s+//g; + $q =~ s/\s+$//g; + $q =~ s/\s+/ /g; + return $q; +} + +sub selectedClauses { + my @clauses = grep { defined } map { $_->value } $agent->current_form->find_input("clauses"); + return [ @clauses ]; +} + + +diag "add the first condition" if $ENV{'TEST_VERBOSE'}; +{ + ok $agent->form_name('BuildQuery'), "found the form once"; + $agent->field("ActorField", "Owner"); + $agent->field("ActorOp", "="); + $agent->field("ValueOfActor", "Nobody"); + $agent->submit; + is getQueryFromForm, "Owner = 'Nobody'", 'correct query'; +} + +diag "set the next condition" if $ENV{'TEST_VERBOSE'}; +{ + ok($agent->form_name('BuildQuery'), "found the form again"); + $agent->field("QueueOp", "!="); + $agent->field("ValueOfQueue", "Regression"); + $agent->submit; + is getQueryFromForm, "Owner = 'Nobody' AND Queue != 'Regression'", + 'correct query'; +} + +diag "We're going to delete the owner" if $ENV{'TEST_VERBOSE'}; +{ + $agent->select("clauses", ["0"] ); + $agent->click("DeleteClause"); + ok $agent->form_name('BuildQuery'), "found the form"; + is getQueryFromForm, "Queue != 'Regression'", 'correct query'; +} + +diag "add a cond with OR and se number by the way" if $ENV{'TEST_VERBOSE'}; +{ + $agent->field("AndOr", "OR"); + $agent->select("idOp", ">"); + $agent->field("ValueOfid" => "1234"); + $agent->click("AddClause"); + ok $agent->form_name('BuildQuery'), "found the form again"; + is getQueryFromForm, "Queue != 'Regression' OR id > 1234", + "added something as OR, and number not quoted"; + is_deeply selectedClauses, ["1"], 'the id that we just entered is still selected'; + +} + +diag "Move the second one up a level" if $ENV{'TEST_VERBOSE'}; +{ + $agent->click("Up"); + ok $agent->form_name('BuildQuery'), "found the form again"; + is getQueryFromForm, "id > 1234 OR Queue != 'Regression'", "moved up one"; + is_deeply selectedClauses, ["0"], 'the one we moved up is selected'; +} + +diag "Move the second one right" if $ENV{'TEST_VERBOSE'}; +{ + $agent->click("Right"); + ok $agent->form_name('BuildQuery'), "found the form again"; + is getQueryFromForm, "Queue != 'Regression' OR ( id > 1234 )", + "moved over to the right (and down)"; + is_deeply selectedClauses, ["2"], 'the one we moved right is selected'; +} + +diag "Move the block up" if $ENV{'TEST_VERBOSE'}; +{ + $agent->select("clauses", ["1"]); + $agent->click("Up"); + ok $agent->form_name('BuildQuery'), "found the form again"; + is getQueryFromForm, "( id > 1234 ) OR Queue != 'Regression'", "moved up"; + is_deeply selectedClauses, ["0"], 'the one we moved up is selected'; +} + + +diag "Can not move up the top most clause" if $ENV{'TEST_VERBOSE'}; +{ + $agent->select("clauses", ["0"]); + $agent->click("Up"); + ok $agent->form_name('BuildQuery'), "found the form again"; + $agent->content_like(qr/error: can\S+t move up/, "i shouldn't have been able to hit up"); + is_deeply selectedClauses, ["0"], 'the one we tried to move is selected'; +} + +diag "Can not move left the left most clause" if $ENV{'TEST_VERBOSE'}; +{ + $agent->click("Left"); + ok($agent->form_name('BuildQuery'), "found the form again"); + $agent->content_like(qr/error: can\S+t move left/, "i shouldn't have been able to hit left"); + is_deeply selectedClauses, ["0"], 'the one we tried to move is selected'; +} + +diag "Add a condition into a nested block" if $ENV{'TEST_VERBOSE'}; +{ + $agent->select("clauses", ["1"]); + $agent->select("ValueOfStatus" => "stalled"); + $agent->submit; + ok $agent->form_name('BuildQuery'), "found the form again"; + is_deeply selectedClauses, ["2"], 'the one we added is only selected'; + is getQueryFromForm, + "( id > 1234 AND Status = 'stalled' ) OR Queue != 'Regression'", + "added new one"; +} + +diag "click advanced, enter 'C1 OR ( C2 AND C3 )', apply, aggregators should stay the same." + if $ENV{'TEST_VERBOSE'}; +{ + my $response = $agent->get($url."Search/Edit.html"); + ok( $response->is_success, "Fetched /Search/Edit.html" ); + ok($agent->form_number(3), "found the form"); + $agent->field("Query", "Status = 'new' OR ( Status = 'open' AND Subject LIKE 'office' )"); + $agent->submit; + is( getQueryFromForm, + "Status = 'new' OR ( Status = 'open' AND Subject LIKE 'office' )", + "no aggregators change" + ); +} + +# - new items go one level down +# - add items at currently selected level +# - if nothing is selected, add at end, one level down +# +# move left +# - error if nothing selected +# - same item should be selected after move +# - can't move left if you're at the top level +# +# move right +# - error if nothing selected +# - same item should be selected after move +# - can always move right (no max depth...should there be?) +# +# move up +# - error if nothing selected +# - same item should be selected after move +# - can't move up if you're first in the list +# +# move down +# - error if nothing selected +# - same item should be selected after move +# - can't move down if you're last in the list +# +# toggle +# - error if nothing selected +# - change all aggregators in the grouping +# - don't change any others +# +# delete +# - error if nothing selected +# - delete currently selected item +# - delete all children of a grouping +# - if delete leaves a node with no children, delete that, too +# - what should be selected? +# +# Clear +# - clears entire query +# - clears it from the session, too + +# }}} + +# create a custom field with nonascii name and try to add a condition +{ + my $cf = RT::CustomField->new( $RT::SystemUser ); + $cf->LoadByName( Name => "\x{442}", Queue => 0 ); + if ( $cf->id ) { + is($cf->Type, 'Freeform', 'loaded and type is correct'); + } else { + my ($return, $msg) = $cf->Create( + Name => "\x{442}", + Queue => 0, + Type => 'Freeform', + ); + ok($return, 'created CF') or diag "error: $msg"; + } + + my $response = $agent->get($url."Search/Build.html?NewQuery=1"); + ok( $response->is_success, "Fetched " . $url."Search/Build.html" ); + + ok($agent->form_name('BuildQuery'), "found the form once"); + $agent->field("ValueOf'CF.{\x{442}}'", "\x{441}"); + $agent->submit(); + is( getQueryFromForm, + "'CF.{\x{442}}' LIKE '\x{441}'", + "no changes, no duplicate condition with badly encoded text" + ); + +} + +diag "input a condition, select (several conditions), click delete" + if $ENV{'TEST_VERBOSE'}; +{ + my $response = $agent->get( $url."Search/Edit.html" ); + ok $response->is_success, "Fetched /Search/Edit.html"; + ok $agent->form_number(3), "found the form"; + $agent->field("Query", "( Status = 'new' OR Status = 'open' )"); + $agent->submit; + is( getQueryFromForm, + "( Status = 'new' OR Status = 'open' )", + "query is the same" + ); + $agent->select("clauses", [qw(0 1 2)]); + $agent->field( ValueOfid => 10 ); + $agent->click("DeleteClause"); + + is( getQueryFromForm, + "id < 10", + "replaced query successfuly" + ); +} + +1; diff --git a/rt/t/web/quicksearch.t b/rt/t/web/quicksearch.t new file mode 100644 index 000000000..cd9a8e76c --- /dev/null +++ b/rt/t/web/quicksearch.t @@ -0,0 +1,51 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 7; +my ($baseurl, $m) = RT::Test->started_ok; +my $url = $m->rt_base_url; + +# merged tickets still show up in search +my $t1 = RT::Ticket->new($RT::SystemUser); +$t1->Create( + Subject => 'base ticket'.$$, + Queue => 'general', + Owner => 'root', + Requestor => 'customsearch@localhost', + MIMEObj => MIME::Entity->build( + From => 'customsearch@localhost', + To => 'rt@localhost', + Subject => 'base ticket'.$$, + Data => "DON'T SEARCH FOR ME", + ), +); +ok(my $id1 = $t1->id, 'created ticket for custom search'); + +my $t2 = RT::Ticket->new($RT::SystemUser); +$t2->Create( + Subject => 'merged away'.$$, + Queue => 'general', + Owner => 'root', + Requestor => 'customsearch@localhost', + MIMEObj => MIME::Entity->build( + From => 'customsearch@localhost', + To => 'rt@localhost', + Subject => 'merged away'.$$, + Data => "MERGEDAWAY", + ), +); +ok(my $id2 = $t2->id, 'created ticket for custom search'); + +my ($ok, $msg) = $t2->MergeInto($id1); +ok($ok, "merge: $msg"); + +ok($m->login, 'logged in'); + +$m->form_with_fields('q'); +$m->field(q => 'fulltext:MERGEDAWAY'); +TODO: { + local $TODO = "We don't yet handle merged ticket content searches right"; +$m->content_contains('Found 1 ticket'); +} +$m->content_contains('base ticket', "base ticket is found, not the merged-away ticket"); diff --git a/rt/t/web/rest-non-ascii-subject.t b/rt/t/web/rest-non-ascii-subject.t new file mode 100644 index 000000000..70c910afe --- /dev/null +++ b/rt/t/web/rest-non-ascii-subject.t @@ -0,0 +1,55 @@ +#!/usr/bin/env perl +# Test ticket creation with REST using non ascii subject +use strict; +use warnings; +use RT::Test tests => 7; + +local $RT::Test::SKIP_REQUEST_WORK_AROUND = 1; + +use Encode; +# \x{XX} where XX is less than 255 is not treated as unicode code point +my $subject = Encode::decode('latin1', "Sujet accentu\x{e9}"); +my $text = Encode::decode('latin1', "Contenu accentu\x{e9}"); + +my ($baseurl, $m) = RT::Test->started_ok; + +my $queue = RT::Test->load_or_create_queue(Name => 'General'); +ok($queue->Id, "loaded the General queue"); + +my $content = "id: ticket/new +Queue: General +Requestor: root +Subject: $subject +Cc: +AdminCc: +Owner: +Status: new +Priority: +InitialPriority: +FinalPriority: +TimeEstimated: +Starts: 2009-03-10 16:14:55 +Due: 2009-03-10 16:14:55 +Text: $text"; + +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', +# error message from HTTP::Message: content must be bytes + content => Encode::encode_utf8($content), +], Content_Type => 'form-data' ); + +my ($id) = $m->content =~ /Ticket (\d+) created/; +ok($id, "got ticket #$id"); + +my $ticket = RT::Ticket->new($RT::SystemUser); +$ticket->Load($id); +is($ticket->Id, $id, "loaded the REST-created ticket"); +is($ticket->Subject, $subject, "ticket subject successfully set"); + +my $attach = $ticket->Transactions->First->Attachments->First; +is($attach->Subject, $subject, "attachement subject successfully set"); +TODO: { + local $TODO = "Not fixed yet, but not a regression"; + is($attach->GetHeader('Subject'), $subject, "attachement header subject successfully set"); +} diff --git a/rt/t/web/rest.t b/rt/t/web/rest.t new file mode 100644 index 000000000..b3a7c558b --- /dev/null +++ b/rt/t/web/rest.t @@ -0,0 +1,71 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use RT::Test tests => 16; + +my ($baseurl, $m) = RT::Test->started_ok; + +for my $name ("severity", "fu()n:k/") { + my $cf = RT::Test->load_or_create_custom_field( + Name => $name, + Type => 'Freeform', + Queue => 'General', + ); + ok($cf->Id, "created a CustomField"); + is($cf->Name, $name, "correct CF name"); +} + +my $queue = RT::Test->load_or_create_queue(Name => 'General'); +ok($queue->Id, "loaded the General queue"); + +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', + format => 'l', +]); + +my $text = $m->content; +my @lines = $text =~ m{.*}g; +shift @lines; # header + +# CFs aren't in the default ticket form +push @lines, "CF-fu()n:k/: maximum"; # old style +push @lines, "CF.{severity}: explosive"; # new style + +$text = join "\n", @lines; + +ok($text =~ s/Subject:\s*$/Subject: REST interface/m, "successfully replaced subject"); + +$m->post("$baseurl/REST/1.0/ticket/edit", [ + user => 'root', + pass => 'password', + + content => $text, +], Content_Type => 'form-data'); + +my ($id) = $m->content =~ /Ticket (\d+) created/; +ok($id, "got ticket #$id"); + +my $ticket = RT::Ticket->new($RT::SystemUser); +$ticket->Load($id); +is($ticket->Id, $id, "loaded the REST-created ticket"); +is($ticket->Subject, "REST interface", "subject successfully set"); +is($ticket->FirstCustomFieldValue("fu()n:k/"), "maximum", "CF successfully set"); + +$m->post("$baseurl/REST/1.0/search/ticket", [ + user => 'root', + pass => 'password', + query => "id=$id", + fields => "Subject,CF-fu()n:k/,CF.{severity},Status", +]); + +# the fields are interpreted server-side a hash (why?), so we can't depend +# on order +for ("id: ticket/1", + "Subject: REST interface", + "CF.{fu()n:k/}: maximum", + "CF.{severity}: explosive", + "Status: new") { + $m->content_contains($_); +} + diff --git a/rt/t/web/rights.t b/rt/t/web/rights.t new file mode 100644 index 000000000..b47ba99af --- /dev/null +++ b/rt/t/web/rights.t @@ -0,0 +1,85 @@ +#!/usr/bin/perl -w +use strict; +use warnings; + +use RT::Test tests => 14; + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, "logged in"; + +$m->follow_link_ok({ text => 'Configuration' }); +$m->follow_link_ok({ text => 'Global' }); +$m->follow_link_ok({ text => 'Group Rights' }); + + +sub get_rights { + my $agent = shift; + my $principal_id = shift; + my $object = shift; + $agent->form_number(3); + my @inputs = $agent->current_form->find_input("RevokeRight-$principal_id-$object"); + my @rights = sort grep $_, map $_->possible_values, grep $_, @inputs; + return @rights; +}; + +diag "load Everyone group" if $ENV{'TEST_VERBOSE'}; +my ($everyone, $everyone_gid); +{ + $everyone = RT::Group->new( $RT::SystemUser ); + $everyone->LoadSystemInternalGroup('Everyone'); + ok($everyone_gid = $everyone->id, "loaded 'everyone' group"); +} + +diag "revoke all global rights from Everyone group" if $ENV{'TEST_VERBOSE'}; +my @has = get_rights( $m, $everyone_gid, 'RT::System-1' ); +if ( @has ) { + $m->form_number(3); + $m->tick("RevokeRight-$everyone_gid-RT::System-1", $_) foreach @has; + $m->submit; + + is_deeply([get_rights( $m, $everyone_gid, 'RT::System-1' )], [], 'deleted all rights' ); +} else { + ok(1, 'the group has no global rights'); +} + +diag "grant SuperUser right to everyone" if $ENV{'TEST_VERBOSE'}; +{ + $m->form_number(3); + $m->select("GrantRight-$everyone_gid-RT::System-1", ['SuperUser']); + $m->submit; + + $m->content_contains('Right Granted', 'got message'); + RT::Principal::InvalidateACLCache(); + ok($everyone->PrincipalObj->HasRight( Right => 'SuperUser', Object => $RT::System ), 'group has right'); + is_deeply( [get_rights( $m, $everyone_gid, 'RT::System-1' )], ['SuperUser'], 'granted SuperUser right' ); +} + +diag "revoke the right" if $ENV{'TEST_VERBOSE'}; +{ + $m->form_number(3); + $m->tick("RevokeRight-$everyone_gid-RT::System-1", 'SuperUser'); + $m->submit; + + $m->content_contains('Right revoked', 'got message'); + RT::Principal::InvalidateACLCache(); + ok(!$everyone->PrincipalObj->HasRight( Right => 'SuperUser', Object => $RT::System ), 'group has no right'); + is_deeply( [get_rights( $m, $everyone_gid, 'RT::System-1' )], [], 'revoked SuperUser right' ); +} + + +diag "return rights the group had in the beginning" if $ENV{'TEST_VERBOSE'}; +if ( @has ) { + $m->form_number(3); + $m->select("GrantRight-$everyone_gid-RT::System-1", \@has); + $m->submit; + + $m->content_contains('Right Granted', 'got message'); + is_deeply( + [ get_rights( $m, $everyone_gid, 'RT::System-1' ) ], + [ @has ], + 'returned back all rights' + ); +} else { + ok(1, 'the group had no global rights, so nothing to return'); +} + diff --git a/rt/t/web/rights1.t b/rt/t/web/rights1.t new file mode 100644 index 000000000..6da204cc9 --- /dev/null +++ b/rt/t/web/rights1.t @@ -0,0 +1,134 @@ +#!/usr/bin/perl -w +use strict; +use HTTP::Cookies; + +use RT::Test tests => 35; +my ($baseurl, $agent) = RT::Test->started_ok; + +# Create a user with basically no rights, to start. +my $user_obj = RT::User->new($RT::SystemUser); +my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('customer-'.$$.'@example.com'); +ok($ret, 'ACL test user creation'); +$user_obj->SetName('customer-'.$$); +$user_obj->SetPrivileged(1); +($ret, $msg) = $user_obj->SetPassword('customer'); +ok($ret, "ACL test password set. $msg"); + +# Now test the web interface, making sure objects come and go as +# required. + + +my $cookie_jar = HTTP::Cookies->new; + +# give the agent a place to stash the cookies + +$agent->cookie_jar($cookie_jar); + +no warnings 'once'; +# get the top page +login($agent, $user_obj); + +# Test for absence of Configure and Preferences tabs. +ok(!$agent->find_link( url => "$RT::WebPath/Admin/", + text => 'Configuration'), "No config tab" ); +ok(!$agent->find_link( url => "$RT::WebPath/User/Prefs.html", + text => 'Preferences'), "No prefs pane" ); + +# Now test for their presence, one at a time. Sleep for a bit after +# ACL changes, thanks to the 10s ACL cache. +my ($grantid,$grantmsg) =$user_obj->PrincipalObj->GrantRight(Right => 'ShowConfigTab', Object => $RT::System); + +ok($grantid,$grantmsg); + +$agent->reload; + +like($agent->{'content'} , qr/Logout/i, "Reloaded page successfully"); +ok($agent->find_link( url => "$RT::WebPath/Admin/", + text => 'Configuration'), "Found config tab" ); +my ($revokeid,$revokemsg) =$user_obj->PrincipalObj->RevokeRight(Right => 'ShowConfigTab'); +ok ($revokeid,$revokemsg); +($grantid,$grantmsg) =$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf'); +ok ($grantid,$grantmsg); +$agent->reload(); +like($agent->{'content'} , qr/Logout/i, "Reloaded page successfully"); +ok($agent->find_link( + text => 'Preferences'), "Found prefs pane" ); +($revokeid,$revokemsg) = $user_obj->PrincipalObj->RevokeRight(Right => 'ModifySelf'); +ok ($revokeid,$revokemsg); +# Good. Now load the search page and test Load/Save Search. +$agent->follow_link( url => "$RT::WebPath/Search/Build.html", + text => 'Tickets'); +is($agent->{'status'}, 200, "Fetched search builder page"); +ok($agent->{'content'} !~ /Load saved search/i, "No search loading box"); +ok($agent->{'content'} !~ /Saved searches/i, "No saved searches box"); + +($grantid,$grantmsg) = $user_obj->PrincipalObj->GrantRight(Right => 'LoadSavedSearch'); +ok($grantid,$grantmsg); +$agent->reload(); +like($agent->{'content'} , qr/Load saved search/i, "Search loading box exists"); +ok($agent->{'content'} !~ /input\s+type=['"]submit['"][^>]+name=['"]SavedSearchSave['"]/i, + "Still no saved searches box"); + +($grantid,$grantmsg) =$user_obj->PrincipalObj->GrantRight(Right => 'CreateSavedSearch'); +ok ($grantid,$grantmsg); +$agent->reload(); +like($agent->{'content'} , qr/Load saved search/i, + "Search loading box still exists"); +like($agent->{'content'} , qr/input\s+type=['"]submit['"][^>]+name=['"]SavedSearchSave['"]/i, + "Saved searches box exists"); + +# Create a group, and a queue, so we can test limited user visibility +# via SelectOwner. + +my $queue_obj = RT::Queue->new($RT::SystemUser); +($ret, $msg) = $queue_obj->Create(Name => 'CustomerQueue-'.$$, + Description => 'queue for SelectOwner testing'); +ok($ret, "SelectOwner test queue creation. $msg"); +my $group_obj = RT::Group->new($RT::SystemUser); +($ret, $msg) = $group_obj->CreateUserDefinedGroup(Name => 'CustomerGroup-'.$$, + Description => 'group for SelectOwner testing'); +ok($ret, "SelectOwner test group creation. $msg"); + +# Add our customer to the customer group, and give it queue rights. +($ret, $msg) = $group_obj->AddMember($user_obj->PrincipalObj->Id()); +ok($ret, "Added customer to its group. $msg"); +($grantid,$grantmsg) =$group_obj->PrincipalObj->GrantRight(Right => 'OwnTicket', + Object => $queue_obj); + +ok($grantid,$grantmsg); +($grantid,$grantmsg) =$group_obj->PrincipalObj->GrantRight(Right => 'SeeQueue', + Object => $queue_obj); +ok ($grantid,$grantmsg); +# Now. When we look at the search page we should be able to see +# ourself in the list of possible owners. + +$agent->reload(); +ok($agent->form_name('BuildQuery'), "Yep, form is still there"); +my $input = $agent->current_form->find_input('ValueOfActor'); +ok(grep(/customer-$$/, $input->value_names()), "Found self in the actor listing"); + +sub login { + my $agent = shift; + + my $url = "http://localhost:" . RT->Config->Get('WebPort') . RT->Config->Get('WebPath') . "/"; + $agent->get($url); + is( $agent->{'status'}, 200, + "Loaded a page - http://localhost" . RT->Config->Get('WebPath') ); + + # {{{ test a login + + # follow the link marked "Login" + + ok( $agent->{form}->find_input('user') ); + + ok( $agent->{form}->find_input('pass') ); + like( $agent->{'content'} , qr/username:/i ); + $agent->field( 'user' => $user_obj->Name ); + $agent->field( 'pass' => 'customer' ); + + # the field isn't named, so we have to click link 0 + $agent->click(0); + is( $agent->{'status'}, 200, "Fetched the page ok" ); + like( $agent->{'content'} , qr/Logout/i, "Found a logout link" ); +} +1; diff --git a/rt/t/web/saved_search_chart.t b/rt/t/web/saved_search_chart.t new file mode 100644 index 000000000..105166233 --- /dev/null +++ b/rt/t/web/saved_search_chart.t @@ -0,0 +1,86 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 19; +my ( $url, $m ) = RT::Test->started_ok; +use RT::Attribute; +my $search = RT::Attribute->new($RT::SystemUser); +my $ticket = RT::Ticket->new($RT::SystemUser); +my ( $ret, $msg ) = $ticket->Create( + Subject => 'base ticket' . $$, + Queue => 'general', + Owner => 'root', + Requestor => 'root@localhost', + MIMEObj => MIME::Entity->build( + From => 'root@localhost', + To => 'rt@localhost', + Subject => 'base ticket' . $$, + Data => "", + ), +); +ok( $ret, "ticket created: $msg" ); + +ok( $m->login, 'logged in' ); + +$m->get_ok( $url . "/Search/Chart.html?Query=" . 'id=1' ); +my ($owner) = $m->content =~ /value="(RT::User-\d+)"/; + +$m->submit_form( + form_name => 'SaveSearch', + fields => { + SavedSearchDescription => 'first chart', + SavedSearchOwner => $owner, + }, + button => 'SavedSearchSave', +); + +$m->content_like( qr/Chart first chart saved/, 'saved first chart' ); + +my ( $search_uri, $id ) = $m->content =~ /value="(RT::User-\d+-SavedSearch-(\d+))"/; +$m->submit_form( + form_name => 'SaveSearch', + fields => { SavedSearchLoad => $search_uri }, +); + +$m->content_like( qr/name="SavedSearchDelete"\s+value="Delete"/, + 'found Delete button' ); +$m->content_like( + qr/name="SavedSearchDescription"\s+value="first chart"/, + 'found Description input with the value filled' +); +$m->content_like( qr/name="SavedSearchSave"\s+value="Update"/, + 'found Update button' ); +$m->content_unlike( qr/name="SavedSearchSave"\s+value="Save"/, + 'no Save button' ); + +$m->submit_form( + form_name => 'SaveSearch', + fields => { + Query => 'id=2', + PrimaryGroupBy => 'Status', + ChartStyle => 'pie', + }, + button => 'SavedSearchSave', +); + +$m->content_like( qr/Chart first chart updated/, 'found updated message' ); +$m->content_like( qr/id=2/, 'Query is updated' ); +$m->content_like( qr/value="Status"\s+selected="selected"/, + 'PrimaryGroupBy is updated' ); +$m->content_like( qr/value="pie"\s+selected="selected"/, + 'ChartType is updated' ); +ok( $search->Load($id) ); +is( $search->SubValue('Query'), 'id=2', 'Query is indeed updated' ); +is( $search->SubValue('PrimaryGroupBy'), + 'Status', 'PrimaryGroupBy is indeed updated' ); +is( $search->SubValue('ChartStyle'), 'pie', 'ChartStyle is indeed updated' ); + +# finally, let's test delete +$m->submit_form( + form_name => 'SaveSearch', + button => 'SavedSearchDelete', +); +$m->content_like( qr/Chart first chart deleted/, 'found deleted message' ); +$m->content_unlike( qr/value="RT::User-\d+-SavedSearch-\d+"/, + 'no saved search' ); diff --git a/rt/t/web/saved_search_permissions.t b/rt/t/web/saved_search_permissions.t new file mode 100644 index 000000000..f91ca13c6 --- /dev/null +++ b/rt/t/web/saved_search_permissions.t @@ -0,0 +1,34 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 10; +my $user = RT::User->new($RT::SystemUser); +ok( + $user->Create( + Name => 'foo', + Privileged => 1, + Password => 'foobar' + ) +); + +my ( $url, $m ) = RT::Test->started_ok; +ok( $m->login, 'root logged in' ); +$m->get_ok( $url . '/Search/Build.html?Query=id<100' ); +$m->submit_form( + form_name => 'BuildQuery', + fields => { SavedSearchDescription => 'test' }, + button => 'SavedSearchSave', +); +$m->content_contains( q{name="SavedSearchDescription" value="test"}, + 'saved test search' ); +my ($id) = $m->content =~ /value="(RT::User-\d+-SavedSearch-\d+)"/; +ok( $m->login( 'foo', 'foobar' ), 'logged in' ); +$m->get_ok( $url . "/Search/Build.html?SavedSearchLoad=$id" ); + +my $message = qq{Can not load saved search "$id"}; +RT::Interface::Web::EscapeUTF8( \$message ); +$m->content_contains( $message, 'user foo can not load saved search of root' ); + +$m->warning_like( qr/User #\d+ tried to load container user #\d+/, + 'get warning' ); diff --git a/rt/t/web/search_bulk_update_links.t b/rt/t/web/search_bulk_update_links.t new file mode 100644 index 000000000..d6bfdfd3c --- /dev/null +++ b/rt/t/web/search_bulk_update_links.t @@ -0,0 +1,147 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 28; +my ( $url, $m ) = RT::Test->started_ok; +ok( $m->login, 'logged in' ); + +my $rtname = RT->Config->Get('rtname'); + +# create tickets +use RT::Ticket; + +my ( @link_tickets, @search_tickets ); +for ( 1 .. 3 ) { + my $link_ticket = RT::Ticket->new($RT::SystemUser); + my ( $ret, $msg ) = $link_ticket->Create( + Subject => "link ticket $_", + Queue => 'general', + Owner => 'root', + Requestor => 'root@localhost', + ); + ok( $ret, "link ticket created: $msg" ); + push @link_tickets, $ret; +} + +for ( 1 .. 3 ) { + my $ticket = RT::Ticket->new($RT::SystemUser); + my ( $ret, $msg ) = $ticket->Create( + Subject => "search ticket $_", + Queue => 'general', + Owner => 'root', + Requestor => 'root@localhost', + ); + ok( $ret, "search ticket created: $msg" ); + push @search_tickets, $ret; +} + +# let's add link to 1 search ticket first +$m->get_ok( $url . "/Search/Bulk.html?Query=id=$search_tickets[0]&Rows=10" ); +$m->content_contains( 'Current Links', 'has current links part' ); +$m->content_lacks( 'DeleteLink--', 'no delete link stuff' ); +$m->submit_form( + form_number => 3, + fields => { + 'Ticket-DependsOn' => $link_tickets[0], + 'Ticket-MemberOf' => $link_tickets[1], + 'Ticket-RefersTo' => $link_tickets[2], + }, +); +$m->content_contains( + "Ticket $search_tickets[0] depends on Ticket $link_tickets[0]", + 'depends on msg', +); +$m->content_contains( + "Ticket $search_tickets[0] member of Ticket $link_tickets[1]", + 'member of msg', +); +$m->content_contains( + "Ticket $search_tickets[0] refers to Ticket $link_tickets[2]", + 'refers to msg', +); + +$m->content_contains( + "DeleteLink--DependsOn-fsck.com-rt://$rtname/ticket/$link_tickets[0]", + 'found depends on link' ); +$m->content_contains( + "DeleteLink--MemberOf-fsck.com-rt://$rtname/ticket/$link_tickets[1]", + 'found member of link' ); +$m->content_contains( + "DeleteLink--RefersTo-fsck.com-rt://$rtname/ticket/$link_tickets[2]", + 'found refers to link' ); + +# here we check the *real* bulk update +my $query = join ' OR ', map { "id=$_" } @search_tickets; +$m->get_ok( $url . "/Search/Bulk.html?Query=$query&Rows=10" ); +$m->content_contains( 'Current Links', 'has current links part' ); +$m->content_lacks( 'DeleteLink--', 'no delete link stuff' ); + +# test DependsOn, MemberOf and RefersTo +$m->submit_form( + form_number => 3, + fields => { + 'Ticket-DependsOn' => $link_tickets[0], + 'Ticket-MemberOf' => $link_tickets[1], + 'Ticket-RefersTo' => $link_tickets[2], + }, +); + +$m->content_contains( + "DeleteLink--DependsOn-fsck.com-rt://$rtname/ticket/$link_tickets[0]", + 'found depends on link' ); +$m->content_contains( + "DeleteLink--MemberOf-fsck.com-rt://$rtname/ticket/$link_tickets[1]", + 'found member of link' ); +$m->content_contains( + "DeleteLink--RefersTo-fsck.com-rt://$rtname/ticket/$link_tickets[2]", + 'found refers to link' ); + +$m->submit_form( + form_number => 3, + fields => { + "DeleteLink--DependsOn-fsck.com-rt://$rtname/ticket/$link_tickets[0]" => + 1, + "DeleteLink--MemberOf-fsck.com-rt://$rtname/ticket/$link_tickets[1]" => + 1, + "DeleteLink--RefersTo-fsck.com-rt://$rtname/ticket/$link_tickets[2]" => + 1, + }, +); + +$m->content_lacks( 'DeleteLink--', 'links are all deleted' ); + +# test DependedOnBy, Members and ReferredToBy + +$m->submit_form( + form_number => 3, + fields => { + 'DependsOn-Ticket' => $link_tickets[0], + 'MemberOf-Ticket' => $link_tickets[1], + 'RefersTo-Ticket' => $link_tickets[2], + }, +); + +$m->content_contains( + "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[0]-DependsOn-", + 'found depended on link' ); +$m->content_contains( + "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[1]-MemberOf-", + 'found members link' ); +$m->content_contains( + "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[2]-RefersTo-", + 'found referrd to link' ); + +$m->submit_form( + form_number => 3, + fields => { + "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[0]-DependsOn-" => + 1, + "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[1]-MemberOf-" => + 1, + "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[2]-RefersTo-" => + 1, + }, +); +$m->content_lacks( 'DeleteLink--', 'links are all deleted' ); + diff --git a/rt/t/web/ticket-create-utf8.t b/rt/t/web/ticket-create-utf8.t new file mode 100644 index 000000000..a4d7ae98d --- /dev/null +++ b/rt/t/web/ticket-create-utf8.t @@ -0,0 +1,83 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use RT::Test tests => 14; + +$RT::Test::SKIP_REQUEST_WORK_AROUND = 1; + +use Encode; + +my $ru_test = "\x{442}\x{435}\x{441}\x{442}"; +my $ru_autoreply = "\x{410}\x{432}\x{442}\x{43e}\x{43e}\x{442}\x{432}\x{435}\x{442}"; +my $ru_support = "\x{43f}\x{43e}\x{434}\x{434}\x{435}\x{440}\x{436}\x{43a}\x{430}"; + +my $q = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $q && $q->id, 'loaded or created queue'; + +RT::Test->set_rights( + Principal => 'Everyone', + Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'], +); + +my ($baseurl, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + +# create a ticket with a subject only +{ + ok $m->goto_create_ticket( $q ), "go to create ticket"; + $m->form_number(3); + $m->field( Subject => $ru_test ); + $m->submit; + + $m->content_like( + qr{<td\s+class="message-header-value"[^>]*>\s*\Q$ru_test\E\s*</td>}i, + 'header on the page' + ); + + my $ticket = RT::Test->last_ticket; + is $ticket->Subject, $ru_test, "correct subject"; +} + +# create a ticket with a subject and content +{ + ok $m->goto_create_ticket( $q ), "go to create ticket"; + $m->form_number(3); + $m->field( Subject => $ru_test ); + $m->field( Content => $ru_support ); + $m->submit; + + $m->content_like( + qr{<td\s+class="message-header-value"[^>]*>\s*\Q$ru_test\E\s*</td>}i, + 'header on the page' + ); + $m->content_like( + qr{\Q$ru_support\E}i, + 'content on the page' + ); + + my $ticket = RT::Test->last_ticket; + is $ticket->Subject, $ru_test, "correct subject"; +} + +# create a ticket with a subject and content +{ + ok $m->goto_create_ticket( $q ), "go to create ticket"; + $m->form_number(3); + $m->field( Subject => $ru_test ); + $m->field( Content => $ru_support ); + $m->submit; + + $m->content_like( + qr{<td\s+class="message-header-value"[^>]*>\s*\Q$ru_test\E\s*</td>}i, + 'header on the page' + ); + $m->content_like( + qr{\Q$ru_support\E}i, + 'content on the page' + ); + + my $ticket = RT::Test->last_ticket; + is $ticket->Subject, $ru_test, "correct subject"; +} diff --git a/rt/t/web/ticket_owner.t b/rt/t/web/ticket_owner.t new file mode 100644 index 000000000..0bacaf1bc --- /dev/null +++ b/rt/t/web/ticket_owner.t @@ -0,0 +1,356 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use RT::Test tests => 91; + +my $queue = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $queue && $queue->id, 'loaded or created queue'; + +my $user_a = RT::Test->load_or_create_user( + Name => 'user_a', Password => 'password', +); +ok $user_a && $user_a->id, 'loaded or created user'; + +my $user_b = RT::Test->load_or_create_user( + Name => 'user_b', Password => 'password', +); +ok $user_b && $user_b->id, 'loaded or created user'; + +RT::Test->started_ok; + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket ReplyToTicket)] }, + { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] }, +), 'set rights'); + +my $agent_a = RT::Test::Web->new; +ok $agent_a->login('user_a', 'password'), 'logged in as user A'; + +diag "current user has no right to own, nobody selected as owner on create" if $ENV{TEST_VERBOSE}; +{ + $agent_a->get_ok('/', 'open home page'); + $agent_a->form_name('CreateTicketInQueue'); + $agent_a->select( 'Queue', $queue->id ); + $agent_a->submit; + + $agent_a->content_like(qr/Create a new ticket/i, 'opened create ticket page'); + my $form = $agent_a->form_name('TicketCreate'); + is $form->value('Owner'), $RT::Nobody->id, 'correct owner selected'; + ok !grep($_ == $user_a->id, $form->find_input('Owner')->possible_values), + 'user A can not own tickets'; + $agent_a->submit; + + $agent_a->content_like(qr/Ticket \d+ created in queue/i, 'created ticket'); + my ($id) = ($agent_a->content =~ /Ticket (\d+) created in queue/); + ok $id, 'found id of the ticket'; + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $id ); + ok $ticket->id, 'loaded the ticket'; + is $ticket->Owner, $RT::Nobody->id, 'correct owner'; +} + +diag "user can chose owner of a new ticket" if $ENV{TEST_VERBOSE}; +{ + $agent_a->get_ok('/', 'open home page'); + $agent_a->form_name('CreateTicketInQueue'); + $agent_a->select( 'Queue', $queue->id ); + $agent_a->submit; + + $agent_a->content_like(qr/Create a new ticket/i, 'opened create ticket page'); + my $form = $agent_a->form_name('TicketCreate'); + is $form->value('Owner'), $RT::Nobody->id, 'correct owner selected'; + + ok grep($_ == $user_b->id, $form->find_input('Owner')->possible_values), + 'user B is listed as potential owner'; + $agent_a->select('Owner', $user_b->id); + $agent_a->submit; + + $agent_a->content_like(qr/Ticket \d+ created in queue/i, 'created ticket'); + my ($id) = ($agent_a->content =~ /Ticket (\d+) created in queue/); + ok $id, 'found id of the ticket'; + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $id ); + ok $ticket->id, 'loaded the ticket'; + is $ticket->Owner, $user_b->id, 'correct owner'; +} + +my $agent_b = RT::Test::Web->new; +ok $agent_b->login('user_b', 'password'), 'logged in as user B'; + +diag "user A can not change owner after create" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_b->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $user_b->id, 'correct owner'; + + # try the following group of tests twice with different agents(logins) + my $test_cb = sub { + my $agent = shift; + $agent->goto_ticket( $id ); + $agent->follow_link_ok({text => 'Basics'}, 'Ticket -> Basics'); + my $form = $agent->form_number(3); + is $form->value('Owner'), $user_b->id, 'correct owner selected'; + $agent->select('Owner', $RT::Nobody->id); + $agent->submit; + + $agent->content_like( + qr/Permission denied/i, + 'no way to change owner after create if you have no rights' + ); + + my $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $id ); + ok $ticket->id, 'loaded the ticket'; + is $ticket->Owner, $user_b->id, 'correct owner'; + }; + + $test_cb->($agent_a); + diag "even owner(user B) can not change owner" if $ENV{TEST_VERBOSE}; + $test_cb->($agent_b); +} + +diag "on reply correct owner is selected" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_b->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $user_b->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + $agent_a->follow_link_ok({text => 'Reply'}, 'Ticket -> Basics'); + + my $form = $agent_a->form_number(3); + is $form->value('Owner'), '', 'empty value selected'; + $agent_a->submit; + + $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $id ); + ok $ticket->id, 'loaded the ticket'; + is $ticket->Owner, $user_b->id, 'correct owner'; +} + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket)] }, + { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] }, +), 'set rights'); + +diag "Couldn't take without coresponding right" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $RT::Nobody->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'no Take link'; + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link as well'; +} + +diag "Couldn't steal without coresponding right" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_b->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $user_b->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link'; + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'no Take link as well'; +} + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket TakeTicket)] }, +), 'set rights'); + +diag "TakeTicket require OwnTicket to work" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $RT::Nobody->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'no Take link'; + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link as well'; +} + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket TakeTicket)] }, + { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] }, +), 'set rights'); + +diag "TakeTicket+OwnTicket work" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $RT::Nobody->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link'; + $agent_a->follow_link_ok({text => 'Take'}, 'Ticket -> Take'); + + $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $id ); + ok $ticket->id, 'loaded the ticket'; + is $ticket->Owner, $user_a->id, 'correct owner'; +} + +diag "TakeTicket+OwnTicket don't work when owner is not nobody" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_b->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $user_b->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'no Take link'; + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link too'; +} + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket StealTicket)] }, + { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] }, +), 'set rights'); + +diag "StealTicket require OwnTicket to work" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_b->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $user_b->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link'; + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'no Take link too'; +} + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket StealTicket)] }, + { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] }, +), 'set rights'); + +diag "StealTicket+OwnTicket work" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_b->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $user_b->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'but no Take link'; + $agent_a->follow_link_ok({text => 'Steal'}, 'Ticket -> Steal'); + + $ticket = RT::Ticket->new( $RT::SystemUser ); + $ticket->Load( $id ); + ok $ticket->id, 'loaded the ticket'; + is $ticket->Owner, $user_a->id, 'correct owner'; +} + +diag "StealTicket+OwnTicket don't work when owner is nobody" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $RT::Nobody->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link'; + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'no Take link as well (no right)'; +} + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket TakeTicket StealTicket)] }, + { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] }, +), 'set rights'); + +diag "no Steal link when owner nobody" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $RT::Nobody->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Steal' ))[0], + 'no Steal link'; + ok( ($agent_a->find_all_links( text => 'Take' ))[0], + 'but have Take link'); +} + +diag "no Take link when owner is not nobody" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($id, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_b->id, + Subject => 'test', + ); + ok $id, 'created a ticket #'. $id or diag "error: $msg"; + is $ticket->Owner, $user_b->id, 'correct owner'; + + $agent_a->goto_ticket( $id ); + ok !($agent_a->find_all_links( text => 'Take' ))[0], + 'no Take link'; + ok( ($agent_a->find_all_links( text => 'Steal' ))[0], + 'but have Steal link'); +} + diff --git a/rt/t/web/ticket_seen.t b/rt/t/web/ticket_seen.t new file mode 100644 index 000000000..00b2632d8 --- /dev/null +++ b/rt/t/web/ticket_seen.t @@ -0,0 +1,80 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use RT::Test tests => 16; + +my $queue = RT::Test->load_or_create_queue( Name => 'Regression' ); +ok $queue && $queue->id, 'loaded or created queue'; + +my $user_a = RT::Test->load_or_create_user( + Name => 'user_a', Password => 'password', +); +ok $user_a && $user_a->id, 'loaded or created user'; + +my $user_b = RT::Test->load_or_create_user( + Name => 'user_b', Password => 'password', +); +ok $user_b && $user_b->id, 'loaded or created user'; + +ok( RT::Test->set_rights( + { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket ModifyTicket)] }, + { Principal => $user_b, Right => [qw(SeeQueue ShowTicket ReplyToTicket)] }, +), 'set rights'); +RT::Test->started_ok; + +my $agent_a = RT::Test::Web->new; +ok $agent_a->login('user_a', 'password'), 'logged in as user A'; + +my $agent_b = RT::Test::Web->new; +ok $agent_b->login('user_b', 'password'), 'logged in as user B'; + +diag "create a ticket for testing" if $ENV{TEST_VERBOSE}; +my $tid; +{ + my $ticket = RT::Ticket->new( $user_a ); + my ($txn, $msg); + ($tid, $txn, $msg) = $ticket->Create( + Queue => $queue->id, + Owner => $user_a->id, + Subject => 'test', + ); + ok $tid, 'created a ticket #'. $tid or diag "error: $msg"; + is $ticket->Owner, $user_a->id, 'correct owner'; +} + +diag "user B adds a message, we check that user A see notification and can clear it" if $ENV{TEST_VERBOSE}; +{ + my $ticket = RT::Ticket->new( $user_b ); + $ticket->Load( $tid ); + ok $ticket->id, 'loaded the ticket'; + + my ($status, $msg) = $ticket->Correspond( Content => 'bla-bla' ); + ok $status, 'added reply' or diag "error: $msg"; + + $agent_a->goto_ticket($tid); + $agent_a->content_like(qr/bla-bla/ims, 'the message on the page'); + + $agent_a->content_like( + qr/unread message/ims, + 'we have not seen something' + ); + + $agent_a->follow_link_ok({text => 'jump to the first unread message and mark all messages as seen'}, 'try to mark all as seen'); + $agent_a->content_like( + qr/Marked all messages as seen/ims, + 'see success message' + ); + + $agent_a->goto_ticket($tid); + $agent_a->content_unlike( + qr/unread message/ims, + 'we have seen everything, so no messages' + ); +} + + + + + diff --git a/rt/t/web/ticket_update_without_content.t b/rt/t/web/ticket_update_without_content.t new file mode 100644 index 000000000..595cb74e9 --- /dev/null +++ b/rt/t/web/ticket_update_without_content.t @@ -0,0 +1,52 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => 10; +my ( $url, $m ) = RT::Test->started_ok; + +# merged tickets still show up in search +my $ticket = RT::Ticket->new($RT::SystemUser); +my ( $ret, $msg ) = $ticket->Create( + Subject => 'base ticket' . $$, + Queue => 'general', + Owner => 'root', + Requestor => 'root@localhost', + MIMEObj => MIME::Entity->build( + From => 'root@localhost', + To => 'rt@localhost', + Subject => 'base ticket' . $$, + Data => "", + ), +); +ok( $ret, "ticket created: $msg" ); + +ok( $m->login, 'logged in' ); + +$m->get_ok( $url . "/Ticket/ModifyAll.html?id=" . $ticket->id ); + +$m->submit_form( + form_number => 3, + fields => { Priority => '1', } +); + +$m->content_like(qr/priority changed/i); +$m->content_unlike(qr/message recorded/i); + +my $root = RT::User->new( $RT::SystemUser ); +$root->Load('root'); +( $ret, $msg ) = $root->SetSignature(<<EOF); +best wishes +foo +EOF + +ok( $ret, $msg ); + +$m->get_ok( $url . "/Ticket/ModifyAll.html?id=" . $ticket->id ); + +$m->submit_form( + form_number => 3, + fields => { Priority => '2', } +); +$m->content_like(qr/priority changed/i); +$m->content_unlike(qr/message recorded/i); diff --git a/rt/t/web/unlimited_search.t b/rt/t/web/unlimited_search.t new file mode 100644 index 000000000..d98baaac0 --- /dev/null +++ b/rt/t/web/unlimited_search.t @@ -0,0 +1,41 @@ +#!/usr/bin/perl + +use strict; + +use RT::Test tests => 8; +RT::Test->started_ok; + +my $ticket = RT::Ticket->new($RT::SystemUser); +for ( 1 .. 75 ) { + $ticket->Create( + Subject => 'Ticket ' . $_, + Queue => 'General', + Owner => 'root', + Requestor => 'unlimitedsearch@localhost', + ); +} + +my $agent = RT::Test::Web->new; +ok $agent->login('root', 'password'), 'logged in as root'; + +$agent->get_ok('/Search/Build.html'); +$agent->form_name('BuildQuery'); +$agent->field('idOp', '>'); +$agent->field('ValueOfid', '0'); +$agent->submit('AddClause'); +$agent->form_name('BuildQuery'); +$agent->field('RowsPerPage', '0'); +$agent->submit('DoSearch'); +$agent->follow_link_ok({text=>'Show Results'}); +$agent->content_like(qr/Ticket 75/); + +$agent->follow_link_ok({text=>'New Search'}); +$agent->form_name('BuildQuery'); +$agent->field('idOp', '>'); +$agent->field('ValueOfid', '0'); +$agent->submit('AddClause'); +$agent->form_name('BuildQuery'); +$agent->field('RowsPerPage', '50'); +$agent->submit('DoSearch'); +$agent->follow_link_ok({text=>'Bulk Update'}); +$agent->content_unlike(qr/Ticket 51/); |