rt 4.0.6
authorIvan Kohler <ivan@freeside.biz>
Thu, 7 Jun 2012 23:58:33 +0000 (16:58 -0700)
committerIvan Kohler <ivan@freeside.biz>
Thu, 7 Jun 2012 23:58:33 +0000 (16:58 -0700)
16 files changed:
rt/etc/upgrade/4.0.6/content [new file with mode: 0644]
rt/etc/upgrade/4.0.6/schema.mysql [new file with mode: 0644]
rt/share/html/Articles/Elements/ShowTopicLink [new file with mode: 0644]
rt/share/html/Elements/CSRF [new file with mode: 0644]
rt/share/html/l_unsafe [new file with mode: 0644]
rt/t/api/report_tickets.t [new file with mode: 0644]
rt/t/mail/dashboard-chart-with-utf8.t [new file with mode: 0644]
rt/t/mail/rfc2231-attachment.t [new file with mode: 0644]
rt/t/mail/specials-in-encodedwords.t [new file with mode: 0644]
rt/t/web/command_line_link_to_articles.t [new file with mode: 0644]
rt/t/web/csrf-rest.t [new file with mode: 0644]
rt/t/web/csrf.t [new file with mode: 0644]
rt/t/web/installer.t [new file with mode: 0644]
rt/t/web/owner_disabled_group_19221.t [new file with mode: 0644]
rt/t/web/query_builder_queue_limits.t [new file with mode: 0644]
rt/t/web/rest_cfs_with_same_name.t [new file with mode: 0644]

diff --git a/rt/etc/upgrade/4.0.6/content b/rt/etc/upgrade/4.0.6/content
new file mode 100644 (file)
index 0000000..dc1a009
--- /dev/null
@@ -0,0 +1,17 @@
+@Initial = (
+    sub {
+        my $txns = RT::Transactions->new( $RT::SystemUser );
+        $txns->Limit(
+            FIELD => "ObjectType",
+            VALUE => "RT::User",
+        );
+        $txns->Limit(
+            FIELD => "Field",
+            VALUE => "Password",
+        );
+        while (my $txn = $txns->Next) {
+            $txn->__Set( Field => $_, Value => '********' )
+                for qw/OldValue NewValue/;
+        }
+    },
+);
diff --git a/rt/etc/upgrade/4.0.6/schema.mysql b/rt/etc/upgrade/4.0.6/schema.mysql
new file mode 100644 (file)
index 0000000..ab32007
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Attributes MODIFY Content LONGBLOB;
diff --git a/rt/share/html/Articles/Elements/ShowTopicLink b/rt/share/html/Articles/Elements/ShowTopicLink
new file mode 100644 (file)
index 0000000..7b6d550
--- /dev/null
@@ -0,0 +1,27 @@
+<%args>
+$Topic
+$Class => 0
+</%args>
+% if ($Link) {
+<a href="Topics.html?id=<% $Topic->Id %>&class=<% $Class %>">\
+% }
+<% $Topic->Name() || loc("(no name)") %>\
+% if ($Topic->Description) {
+: <% $Topic->Description %>
+% }
+
+% if ( $Articles->Count ) {
+ (<&|/l, $Articles->Count &>[quant,_1,article]</&>)
+% }
+
+% if ($Link) {
+</a>
+% }
+
+<%init>
+my $Articles = RT::ObjectTopics->new( $session{'CurrentUser'} );
+$Articles->Limit( FIELD => 'ObjectType', VALUE => 'RT::Article' );
+$Articles->Limit( FIELD => 'Topic',      VALUE => $Topic->Id );
+
+my $Link = $Topic->Children->Count || $Articles->Count;
+</%init>
diff --git a/rt/share/html/Elements/CSRF b/rt/share/html/Elements/CSRF
new file mode 100644 (file)
index 0000000..4893c12
--- /dev/null
@@ -0,0 +1,74 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+%#                                          <sales@bestpractical.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/licenses/old-licenses/gpl-2.0.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 }}}
+<& /Elements/Header, Title => loc('Possible cross-site request forgery') &>
+<& /Elements/Tabs &>
+
+<h1><&|/l&>Possible cross-site request forgery</&></h1>
+
+% my $strong_start = "<strong>";
+% my $strong_end   = "</strong>";
+<p><&|/l_unsafe, $strong_start, $strong_end, $Reason &>RT has detected a possible [_1]cross-site request forgery[_2] for this request, because [_3].  This is possibly caused by a malicious attacker trying to perform actions against RT on your behalf. If you did not initiate this request, then you should alert your security team.</&></p>
+
+% my $start = qq|<strong><a href="$url_with_token">|;
+% my $end   = qq|</a></strong>|;
+<p><&|/l_unsafe, $escaped_path, $start, $end &>If you really intended to visit [_1], then [_2]click here to resume your request[_3].</&></p>
+
+<& /Elements/Footer, %ARGS &>
+% $m->abort;
+<%ARGS>
+$OriginalURL => ''
+$Reason => ''
+$Token => ''
+</%ARGS>
+<%INIT>
+my $escaped_path = $m->interp->apply_escapes($OriginalURL, 'h');
+$escaped_path = "<tt>$escaped_path</tt>";
+
+my $url_with_token = URI->new($OriginalURL);
+$url_with_token->query_form([CSRF_Token => $Token]);
+</%INIT>
diff --git a/rt/share/html/l_unsafe b/rt/share/html/l_unsafe
new file mode 100644 (file)
index 0000000..6396bc6
--- /dev/null
@@ -0,0 +1,52 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+%#                                          <sales@bestpractical.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/licenses/old-licenses/gpl-2.0.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 }}}
+<%init>
+ my $hand = ($session{'CurrentUser'} ||= RT::CurrentUser->new)->LanguageHandle;
+ $m->print($hand->maketext($m->content,@_));
+ return(1);
+</%init>
diff --git a/rt/t/api/report_tickets.t b/rt/t/api/report_tickets.t
new file mode 100644 (file)
index 0000000..4144c60
--- /dev/null
@@ -0,0 +1,15 @@
+use strict;
+use warnings;
+use RT::Test tests => 5;
+
+use RT::Report::Tickets;
+
+my $ticket = RT::Test->create_ticket( Queue => 'General', Subject => 'test' );
+
+my $tickets = RT::Report::Tickets->new( RT->SystemUser );
+$tickets->FromSQL('Updated <= "tomorrow"');
+is( $tickets->Count, 1, "search with transaction join and positive results" );
+
+$tickets->FromSQL('Updated < "yesterday"');
+is( $tickets->Count, 0, "search with transaction join and 0 results" );
+
diff --git a/rt/t/mail/dashboard-chart-with-utf8.t b/rt/t/mail/dashboard-chart-with-utf8.t
new file mode 100644 (file)
index 0000000..6d07b96
--- /dev/null
@@ -0,0 +1,82 @@
+use strict;
+use warnings;
+
+use RT::Test tests => 15;
+use utf8;
+
+my $root = RT::Test->load_or_create_user( Name => 'root' );
+
+my ( $baseurl, $m ) = RT::Test->started_ok;
+ok( $m->login, 'logged in' );
+my $ticket = RT::Ticket->new( $RT::SystemUser );
+$ticket->Create(
+    Queue   => 'General',
+    Subject => 'test äöü',
+);
+ok( $ticket->id, 'created ticket' );
+
+$m->get_ok(q{/Search/Chart.html?Query=Subject LIKE 'test äöü'});
+$m->submit_form(
+    form_name => 'SaveSearch',
+    fields    => {
+        SavedSearchDescription => 'chart foo',
+        SavedSearchOwner       => 'RT::User-' . $root->id,
+    },
+    button => 'SavedSearchSave',
+);
+
+# first, create and populate a dashboard
+$m->get_ok('/Dashboards/Modify.html?Create=1');
+$m->form_name('ModifyDashboard');
+$m->field( 'Name' => 'dashboard foo' );
+$m->click_button( value => 'Create' );
+
+$m->follow_link_ok( { text => 'Content' } );
+my $form  = $m->form_name('Dashboard-Searches-body');
+my @input = $form->find_input('Searches-body-Available');
+my ($dashboards_component) =
+  map { ( $_->possible_values )[1] }
+  grep { ( $_->value_names )[1] =~ /^Chart/ } @input;
+$form->value( 'Searches-body-Available' => $dashboards_component );
+$m->click_button( name => 'add' );
+$m->content_contains('Dashboard updated');
+
+$m->follow_link_ok( { text => 'Subscription' } );
+$m->form_name('SubscribeDashboard');
+$m->field( 'Frequency' => 'daily' );
+$m->field( 'Hour'      => '06:00' );
+$m->click_button( name => 'Save' );
+$m->content_contains('Subscribed to dashboard dashboard foo');
+
+my $c     = $m->get(q{/Search/Chart?Query=Subject LIKE 'test äöü'});
+my $image = $c->content;
+RT::Test->run_and_capture(
+    command => $RT::SbinPath . '/rt-email-dashboards', all => 1
+);
+
+my @mails = RT::Test->fetch_caught_mails;
+is @mails, 1, "got a dashboard mail";
+
+# can't use parse_mail here is because it deletes all attachments
+# before we can call bodyhandle :/
+use RT::EmailParser;
+my $parser = RT::EmailParser->new;
+my $mail = $parser->ParseMIMEEntityFromScalar( $mails[0] );
+like(
+    $mail->head->get('Subject'),
+    qr/Daily Dashboard: dashboard foo/,
+    'mail subject'
+);
+
+my ($mail_image) = grep { $_->mime_type eq 'image/png' } $mail->parts;
+ok( $mail_image, 'mail contains image attachment' );
+
+my $handle = $mail_image->bodyhandle;
+
+my $mail_image_data = '';
+if ( my $io = $handle->open('r') ) {
+    while ( defined( $_ = $io->getline ) ) { $mail_image_data .= $_ }
+    $io->close;
+}
+is( $mail_image_data, $image, 'image in mail is the same one in web' );
+
diff --git a/rt/t/mail/rfc2231-attachment.t b/rt/t/mail/rfc2231-attachment.t
new file mode 100644 (file)
index 0000000..fc74c47
--- /dev/null
@@ -0,0 +1,28 @@
+use strict;
+use warnings;
+
+use utf8;
+use RT::Test tests => undef;
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in as root';
+
+diag "encoded attachment filename with parameter continuations";
+{
+    my $mail = RT::Test->file_content(
+        RT::Test::get_relocatable_file(
+            'rfc2231-attachment-filename-continuations',
+            (File::Spec->updir(), 'data', 'emails')
+        )
+    );
+
+    my ( $status, $id ) = RT::Test->send_via_mailgate($mail);
+    is( $status >> 8, 0, "The mail gateway exited normally" );
+    ok( $id, "Created ticket" );
+
+    $m->get_ok("/Ticket/Display.html?id=$id");
+    $m->content_contains("新しいテキスト ドキュメント.txt", "found full filename");
+}
+
+undef $m;
+done_testing;
+
diff --git a/rt/t/mail/specials-in-encodedwords.t b/rt/t/mail/specials-in-encodedwords.t
new file mode 100644 (file)
index 0000000..f9da9c6
--- /dev/null
@@ -0,0 +1,40 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+diag "specials (, and ;) in MIME encoded-words aren't treated as specials";
+{
+    # RT decodes too early in the game (i.e. before parsing), so it needs to
+    # ensure special characters in encoded words are properly escaped/quoted
+    # after decoding
+
+    RT->Config->Set( ParseNewMessageForTicketCcs => 1 );
+    my $mail = <<'.';
+From: root@localhost
+Subject: testing mime encoded specials
+Cc: a@example.com, =?utf8?q?d=40example.com=2ce=40example.com=3b?=
+    <b@example.com>; c@example.com
+Content-Type: text/plain; charset=utf8
+
+here's some content
+.
+
+    my ( $status, $id ) = RT::Test->send_via_mailgate($mail);
+    is( $status >> 8, 0, "The mail gateway exited normally" );
+    ok( $id, "Created ticket" );
+
+    my $ticket = RT::Ticket->new( RT->SystemUser );
+    $ticket->Load($id);
+    ok $ticket->id, 'loaded ticket';
+
+    my @cc = @{$ticket->Cc->UserMembersObj->ItemsArrayRef};
+    is scalar @cc, 3, "three ccs";
+    for my $addr (qw(a b c)) {
+        ok( (scalar grep { $_->EmailAddress eq "$addr\@example.com" } @cc),
+            "found $addr" );
+    }
+}
+
+done_testing;
+
diff --git a/rt/t/web/command_line_link_to_articles.t b/rt/t/web/command_line_link_to_articles.t
new file mode 100644 (file)
index 0000000..9a49145
--- /dev/null
@@ -0,0 +1,48 @@
+use strict;
+use warnings;
+use Test::Expect;
+use RT::Test tests => 12, actual_server => 1;
+
+my $class = RT::Class->new( RT->SystemUser );
+my ( $class_id, $msg ) = $class->Create( Name => 'foo' );
+ok( $class_id, $msg );
+
+my $article = RT::Article->new( RT->SystemUser );
+( my $article_id, $msg ) =
+  $article->Create( Class => 'foo', Summary => 'article summary' );
+ok( $article_id, $msg );
+
+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';
+$ENV{'RTCONFIG'} = '/dev/null';
+
+expect_run(
+    command => "$rt_tool_path shell",
+    prompt  => 'rt> ',
+    quit    => 'quit',
+);
+expect_send( q{create -t ticket set subject='new ticket'},
+    "creating a ticket..." );
+
+expect_like( qr/Ticket \d+ created/, "created the ticket" );
+expect_handle->before() =~ /Ticket (\d+) created/;
+my $ticket_id = $1;
+expect_send(
+    "link $ticket_id RefersTo a:$article_id",
+    "link $ticket_id RefersTo a:$article_id"
+);
+expect_like( qr/Created link $ticket_id RefersTo a:$article_id/,
+    'created link' );
+expect_send( "show -s ticket/$ticket_id/links", "show ticket links" );
+expect_like( qr|RefersTo: fsck\.com-article://example\.com/article/$article_id|,
+    "found new created link" );
+
+expect_quit();
+
diff --git a/rt/t/web/csrf-rest.t b/rt/t/web/csrf-rest.t
new file mode 100644 (file)
index 0000000..5bb9081
--- /dev/null
@@ -0,0 +1,77 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+# Get a non-REST session
+diag "Standard web session";
+ok $m->login, 'logged in';
+$m->content_contains("RT at a glance", "Get full UI content");
+
+# Requesting a REST page should be fine, as we have a Referer
+$m->post("$baseurl/REST/1.0/ticket/new", [
+    format  => 'l',
+]);
+$m->content_like(qr{^id: ticket/new}m, "REST request with referrer");
+
+# Removing the Referer header gets us an interstitial
+$m->add_header(Referer => undef);
+$m->post("$baseurl/REST/1.0/ticket/new", [
+    format  => 'l',
+    foo     => 'bar',
+]);
+$m->content_contains("Possible cross-site request forgery",
+                 "REST request without referrer is blocked");
+
+# But passing username and password lets us though
+$m->post("$baseurl/REST/1.0/ticket/new", [
+    user    => 'root',
+    pass    => 'password',
+    format  => 'l',
+]);
+$m->content_like(qr{^id: ticket/new}m, "REST request without referrer, but username/password supplied, is OK");
+
+# And we can still access non-REST urls
+$m->get("$baseurl");
+$m->content_contains("RT at a glance", "Full UI is still available");
+
+
+# Now go get a REST session
+diag "REST session";
+$m = RT::Test::Web->new;
+$m->post("$baseurl/REST/1.0/ticket/new", [
+    user    => 'root',
+    pass    => 'password',
+    format  => 'l',
+]);
+$m->content_like(qr{^id: ticket/new}m, "REST request to log in");
+
+# Requesting that page again, with a username/password but no referrer,
+# is fine
+$m->add_header(Referer => undef);
+$m->post("$baseurl/REST/1.0/ticket/new", [
+    user    => 'root',
+    pass    => 'password',
+    format  => 'l',
+]);
+$m->content_like(qr{^id: ticket/new}m, "REST request with no referrer, but username/pass");
+
+# And it's still fine without both referer and username and password,
+# because REST is special-cased
+$m->post("$baseurl/REST/1.0/ticket/new", [
+    format  => 'l',
+]);
+$m->content_like(qr{^id: ticket/new}m, "REST request with no referrer or username/pass is special-cased for REST sessions");
+
+# But the REST page can't request normal pages
+$m->get("$baseurl");
+$m->content_lacks("RT at a glance", "Full UI is denied for REST sessions");
+$m->content_contains("This login session belongs to a REST client", "Tells you why");
+$m->warning_like(qr/This login session belongs to a REST client/, "Logs a warning");
+
+undef $m;
+done_testing;
+
diff --git a/rt/t/web/csrf.t b/rt/t/web/csrf.t
new file mode 100644 (file)
index 0000000..d99b4ce
--- /dev/null
@@ -0,0 +1,181 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+my $ticket = RT::Ticket->new(RT::CurrentUser->new('root'));
+my ($ok, $msg) = $ticket->Create(Queue => 1, Owner => 'nobody', Subject => 'bad music');
+ok($ok);
+my $other = RT::Test->load_or_create_queue(Name => "Other queue", Disabled => 0);
+my $other_queue_id = $other->id;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+my $test_page = "/Ticket/Create.html?Queue=1";
+my $test_path = "/Ticket/Create.html";
+
+ok $m->login, 'logged in';
+
+# valid referer
+$m->add_header(Referer => $baseurl);
+$m->get_ok($test_page);
+$m->content_lacks("Possible cross-site request forgery");
+$m->title_is('Create a new ticket');
+
+# off-site referer BUT provides auth
+$m->add_header(Referer => 'http://example.net');
+$m->get_ok("$test_page&user=root&pass=password");
+$m->content_lacks("Possible cross-site request forgery");
+$m->title_is('Create a new ticket');
+
+# explicitly no referer BUT provides auth
+$m->add_header(Referer => undef);
+$m->get_ok("$test_page&user=root&pass=password");
+$m->content_lacks("Possible cross-site request forgery");
+$m->title_is('Create a new ticket');
+
+# now send a referer from an attacker
+$m->add_header(Referer => 'http://example.net');
+$m->get_ok($test_page);
+$m->content_contains("Possible cross-site request forgery");
+$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>");
+$m->content_contains("the Referrer header supplied by your browser (example.net:80) is not allowed");
+$m->title_is('Possible cross-site request forgery');
+
+# reinstate mech's usual header policy
+$m->delete_header('Referer');
+
+# clicking the resume request button gets us to the test page
+$m->follow_link(text_regex => qr{resume your request});
+$m->content_lacks("Possible cross-site request forgery");
+like($m->response->request->uri, qr{^http://[^/]+\Q$test_path\E\?CSRF_Token=\w+$});
+$m->title_is('Create a new ticket');
+
+# try a whitelisted argument from an attacker
+$m->add_header(Referer => 'http://example.net');
+$m->get_ok("/Ticket/Display.html?id=1");
+$m->content_lacks("Possible cross-site request forgery");
+$m->title_is('#1: bad music');
+
+# now a non-whitelisted argument
+$m->get_ok("/Ticket/Display.html?id=1&Action=Take");
+$m->content_contains("Possible cross-site request forgery");
+$m->content_contains("If you really intended to visit <tt>/Ticket/Display.html</tt>");
+$m->content_contains("the Referrer header supplied by your browser (example.net:80) is not allowed");
+$m->title_is('Possible cross-site request forgery');
+
+$m->delete_header('Referer');
+$m->follow_link(text_regex => qr{resume your request});
+$m->content_lacks("Possible cross-site request forgery");
+like($m->response->request->uri, qr{^http://[^/]+\Q/Ticket/Display.html});
+$m->title_is('#1: bad music');
+$m->content_contains('Owner changed from Nobody to root');
+
+# force mech to never set referer
+$m->add_header(Referer => undef);
+$m->get_ok($test_page);
+$m->content_contains("Possible cross-site request forgery");
+$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>");
+$m->content_contains("your browser did not supply a Referrer header");
+$m->title_is('Possible cross-site request forgery');
+
+$m->follow_link(text_regex => qr{resume your request});
+$m->content_lacks("Possible cross-site request forgery");
+is($m->response->redirects, 0, "no redirection");
+like($m->response->request->uri, qr{^http://[^/]+\Q$test_path\E\?CSRF_Token=\w+$});
+$m->title_is('Create a new ticket');
+
+# try sending the wrong csrf token, then the right one
+$m->add_header(Referer => undef);
+$m->get_ok($test_page);
+$m->content_contains("Possible cross-site request forgery");
+$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>");
+$m->content_contains("your browser did not supply a Referrer header");
+$m->title_is('Possible cross-site request forgery');
+
+# Sending a wrong CSRF is just a normal request.  We'll make a request
+# with just an invalid token, which means no Queue=, which means
+# Create.html errors out.
+my $link = $m->find_link(text_regex => qr{resume your request});
+(my $broken_url = $link->url) =~ s/(CSRF_Token)=\w+/$1=crud/;
+$m->get_ok($broken_url);
+$m->content_contains("Queue could not be loaded");
+$m->title_is('RT Error');
+$m->warning_like(qr/Queue could not be loaded/);
+
+# The token doesn't work for other pages, or other arguments to the same page.
+$m->add_header(Referer => undef);
+$m->get_ok($test_page);
+$m->content_contains("Possible cross-site request forgery");
+my ($token) = $m->content =~ m{CSRF_Token=(\w+)};
+
+$m->add_header(Referer => undef);
+$m->get_ok("/Admin/Queues/Modify.html?id=new&Name=test&CSRF_Token=$token");
+$m->content_contains("Possible cross-site request forgery");
+$m->content_contains("If you really intended to visit <tt>/Admin/Queues/Modify.html</tt>");
+$m->content_contains("your browser did not supply a Referrer header");
+$m->title_is('Possible cross-site request forgery');
+
+$m->follow_link(text_regex => qr{resume your request});
+$m->content_lacks("Possible cross-site request forgery");
+$m->title_is('Configuration for queue test');
+
+# Try the same page, but different query parameters, which are blatted by the token
+$m->get_ok("/Ticket/Create.html?Queue=$other_queue_id&CSRF_Token=$token");
+$m->content_lacks("Possible cross-site request forgery");
+$m->title_is('Create a new ticket');
+$m->text_unlike(qr/Queue:\s*Other queue/);
+$m->text_like(qr/Queue:\s*General/);
+
+# Ensure that file uploads work across the interstitial
+$m->delete_header('Referer');
+$m->get_ok($test_page);
+$m->content_contains("Create a new ticket", 'ticket create page');
+$m->form_name('TicketCreate');
+$m->field('Subject', 'Attachments test');
+
+my $logofile = "$RT::MasonComponentRoot/NoAuth/images/bpslogo.png";
+open LOGO, "<", $logofile or die "Can't open logo file: $!";
+binmode LOGO;
+my $logo_contents = do {local $/; <LOGO>};
+close LOGO;
+$m->field('Attach',  $logofile);
+
+# Lose the referer before the POST
+$m->add_header(Referer => undef);
+$m->submit;
+$m->content_contains("Possible cross-site request forgery");
+$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>");
+$m->follow_link(text_regex => qr{resume your request});
+$m->content_contains('Download bpslogo.png', 'page has file name');
+$m->follow_link_ok({text => "Download bpslogo.png"});
+is($m->content, $logo_contents, "Binary content matches");
+
+
+# now try self-service with CSRF
+my $user = RT::User->new(RT->SystemUser);
+$user->Create(Name => "SelfService", Password => "chops", Privileged => 0);
+
+$m = RT::Test::Web->new;
+$m->get_ok("$baseurl/index.html?user=SelfService&pass=chops");
+$m->title_is("Open tickets", "got self-service interface");
+$m->content_contains("My open tickets", "got self-service interface");
+
+# post without referer
+$m->add_header(Referer => undef);
+$m->get_ok("/SelfService/Create.html?Queue=1");
+$m->content_contains("Possible cross-site request forgery");
+$m->content_contains("If you really intended to visit <tt>/SelfService/Create.html</tt>");
+$m->content_contains("your browser did not supply a Referrer header");
+$m->title_is('Possible cross-site request forgery');
+
+$m->follow_link(text_regex => qr{resume your request});
+$m->content_lacks("Possible cross-site request forgery");
+is($m->response->redirects, 0, "no redirection");
+like($m->response->request->uri, qr{^http://[^/]+\Q/SelfService/Create.html\E\?CSRF_Token=\w+$});
+$m->title_is('Create a ticket');
+$m->content_contains('Describe the issue below:');
+
+undef $m;
+done_testing;
diff --git a/rt/t/web/installer.t b/rt/t/web/installer.t
new file mode 100644 (file)
index 0000000..4dc82df
--- /dev/null
@@ -0,0 +1,95 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+$ENV{RT_TEST_WEB_HANDLER} = 'plack+rt-server';
+use RT::Test
+    tests       => undef,
+    nodb        => 1,
+    server_ok   => 1;
+
+my ($base, $m) = RT::Test->started_ok;
+
+$m->warning_like(qr/If this is a new installation of RT/,
+                 "Got startup warning");
+
+$m->get_ok($base);
+like $m->uri, qr/Install/, 'at installer';
+
+diag "Testing language change";
+{
+    $m->submit_form_ok(
+        {
+            with_fields => {
+                Lang => 'fr',
+            },
+            button => 'ChangeLang',
+        },
+        'change language to french'
+    );
+    $m->content_like(qr/RT\s+pour\s+example\.com/i);
+    $m->submit_form_ok(
+        {
+            with_fields => {
+                Lang => 'en',
+            },
+            button => 'ChangeLang',
+        },
+        'change language to english'
+    );
+    $m->content_like(qr/RT\s+for\s+example\.com/i);
+}
+
+diag "Walking through install screens setting defaults";
+{
+    $m->click_ok('Run');
+
+    # Database type
+    $m->content_contains('DatabaseType');
+    $m->content_contains($_, "found database $_")
+        for qw(MySQL PostgreSQL Oracle SQLite);
+    $m->submit();
+
+    # Database details
+    $m->content_contains('DatabaseName');
+    $m->submit();
+    $m->content_contains('Connection succeeded');
+    $m->submit_form_ok({ button => 'Next' });
+
+    # Basic options
+    $m->submit_form_ok({
+        with_fields => {
+            Password    => 'password',
+        }
+    }, 'set root password');
+
+    # Mail options
+    $m->submit_form_ok({
+        with_fields => {
+            OwnerEmail  => 'admin@example.com',
+        },
+    }, 'set admin email');
+
+    # Mail addresses
+    $m->submit_form_ok({
+        with_fields => {
+            CorrespondAddress   => 'rt@example.com',
+            CommentAddress      => 'rt-comment@example.com',
+        },
+    }, 'set addresses');
+
+    # Initialize database
+    $m->content_contains('database');
+    $m->submit();
+
+    # Finish
+    $m->content_contains('/RT_SiteConfig.pm');
+    $m->content_contains('Finish');
+    $m->submit();
+
+    $m->content_contains('Login');
+    ok $m->login(), 'logged in';
+}
+
+undef $m;
+done_testing;
diff --git a/rt/t/web/owner_disabled_group_19221.t b/rt/t/web/owner_disabled_group_19221.t
new file mode 100644 (file)
index 0000000..2664c5b
--- /dev/null
@@ -0,0 +1,190 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+my $queue = RT::Test->load_or_create_queue( Name => 'Test' );
+ok $queue && $queue->id, 'loaded or created queue';
+
+my $user = RT::Test->load_or_create_user(
+    Name        => 'ausername',
+    Privileged  => 1,
+);
+ok $user && $user->id, 'loaded or created user';
+
+my $group = RT::Group->new(RT->SystemUser);
+my ($ok, $msg) = $group->CreateUserDefinedGroup(Name => 'Disabled Group');
+ok($ok, $msg);
+
+($ok, $msg) = $group->AddMember( $user->PrincipalId );
+ok($ok, $msg);
+
+ok( RT::Test->set_rights({
+    Principal   => $group,
+    Object      => $queue,
+    Right       => [qw(OwnTicket)]
+}), 'set rights');
+
+RT->Config->Set( AutocompleteOwners => 0 );
+my ($base, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in';
+
+diag "user from group shows up in create form";
+{
+    $m->get_ok('/', 'open home page');
+    $m->form_name('CreateTicketInQueue');
+    $m->select( 'Queue', $queue->id );
+    $m->submit;
+
+    $m->content_contains('Create a new ticket', 'opened create ticket page');
+    my $form = $m->form_name('TicketCreate');
+    my $input = $form->find_input('Owner');
+    is $input->value, RT->Nobody->Id, 'correct owner selected';
+    ok((scalar grep { $_ == $user->Id } $input->possible_values), 'user from group is in dropdown');
+}
+
+diag "user from disabled group DOESN'T shows up in create form";
+{
+    ($ok, $msg) = $group->SetDisabled(1);
+    ok($ok, $msg);
+
+    $m->get_ok('/', 'open home page');
+    $m->form_name('CreateTicketInQueue');
+    $m->select( 'Queue', $queue->id );
+    $m->submit;
+
+    $m->content_contains('Create a new ticket', 'opened create ticket page');
+    my $form = $m->form_name('TicketCreate');
+    my $input = $form->find_input('Owner');
+    is $input->value, RT->Nobody->Id, 'correct owner selected';
+    ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown');
+    ($ok, $msg) = $group->SetDisabled(0);
+    ok($ok, $msg);
+}
+
+
+
+diag "Put us in a nested group";
+my $super = RT::Group->new(RT->SystemUser);
+($ok, $msg) = $super->CreateUserDefinedGroup(Name => 'Supergroup');
+ok($ok, $msg);
+
+($ok, $msg) = $super->AddMember( $group->PrincipalId );
+ok($ok, $msg);
+
+ok( RT::Test->set_rights({
+    Principal   => $super,
+    Object      => $queue,
+    Right       => [qw(OwnTicket)]
+}), 'set rights');
+
+
+diag "Disable the middle group";
+{
+    ($ok, $msg) = $group->SetDisabled(1);
+    ok($ok, "Disabled group: $msg");
+
+    $m->get_ok('/', 'open home page');
+    $m->form_name('CreateTicketInQueue');
+    $m->select( 'Queue', $queue->id );
+    $m->submit;
+
+    $m->content_contains('Create a new ticket', 'opened create ticket page');
+    my $form = $m->form_name('TicketCreate');
+    my $input = $form->find_input('Owner');
+    is $input->value, RT->Nobody->Id, 'correct owner selected';
+    ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown');
+    ($ok, $msg) = $group->SetDisabled(0);
+    ok($ok, "Re-enabled group: $msg");
+}
+
+diag "Disable the top group";
+{
+    ($ok, $msg) = $super->SetDisabled(1);
+    ok($ok, "Disabled supergroup: $msg");
+
+    $m->get_ok('/', 'open home page');
+    $m->form_name('CreateTicketInQueue');
+    $m->select( 'Queue', $queue->id );
+    $m->submit;
+
+    $m->content_contains('Create a new ticket', 'opened create ticket page');
+    my $form = $m->form_name('TicketCreate');
+    my $input = $form->find_input('Owner');
+    is $input->value, RT->Nobody->Id, 'correct owner selected';
+    ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown');
+    ($ok, $msg) = $super->SetDisabled(0);
+    ok($ok, "Re-enabled supergroup: $msg");
+}
+
+
+diag "Check WithMember and WithoutMember recursively";
+{
+    my $with = RT::Groups->new( RT->SystemUser );
+    $with->WithMember( PrincipalId => $user->PrincipalObj->Id, Recursively => 1 );
+    $with->Limit( FIELD => 'domain', OPERATOR => '=', VALUE => 'UserDefined' );
+    is_deeply(
+        [map {$_->Name} @{$with->ItemsArrayRef}],
+        ['Disabled Group','Supergroup'],
+        "Get expected recursive memberships",
+    );
+
+    my $without = RT::Groups->new( RT->SystemUser );
+    $without->WithoutMember( PrincipalId => $user->PrincipalObj->Id, Recursively => 1 );
+    $without->Limit( FIELD => 'domain', OPERATOR => '=', VALUE => 'UserDefined' );
+    is_deeply(
+        [map {$_->Name} @{$without->ItemsArrayRef}],
+        [],
+        "And not a member of no groups",
+    );
+
+    ($ok, $msg) = $super->SetDisabled(1);
+    ok($ok, "Disabled supergroup: $msg");
+    $with->RedoSearch;
+    $without->RedoSearch;
+    is_deeply(
+        [map {$_->Name} @{$with->ItemsArrayRef}],
+        ['Disabled Group'],
+        "Recursive check only contains subgroup",
+    );
+    is_deeply(
+        [map {$_->Name} @{$without->ItemsArrayRef}],
+        [],
+        "Doesn't find the currently disabled group",
+    );
+    ($ok, $msg) = $super->SetDisabled(0);
+    ok($ok, "Re-enabled supergroup: $msg");
+
+    ($ok, $msg) = $group->SetDisabled(1);
+    ok($ok, "Disabled intermediate group: $msg");
+    $with->RedoSearch;
+    $without->RedoSearch;
+    is_deeply(
+        [map {$_->Name} @{$with->ItemsArrayRef}],
+        [],
+        "Recursive check finds no groups",
+    );
+    is_deeply(
+        [map {$_->Name} @{$without->ItemsArrayRef}],
+        ['Supergroup'],
+        "Now not a member of the supergroup",
+    );
+    ($ok, $msg) = $group->SetDisabled(0);
+    ok($ok, "Re-enabled intermediate group: $msg");
+}
+
+diag "Check MemberOfGroup";
+{
+    ($ok, $msg) = $group->SetDisabled(1);
+    ok($ok, "Disabled intermediate group: $msg");
+    my $users = RT::Users->new(RT->SystemUser);
+    $users->MemberOfGroup($super->PrincipalObj->id);
+    is($users->Count, 0, "Supergroup claims no members");
+    ($ok, $msg) = $group->SetDisabled(0);
+    ok($ok, "Re-enabled intermediate group: $msg");
+}
+
+
+undef $m;
+done_testing;
diff --git a/rt/t/web/query_builder_queue_limits.t b/rt/t/web/query_builder_queue_limits.t
new file mode 100644 (file)
index 0000000..a3b9765
--- /dev/null
@@ -0,0 +1,180 @@
+use strict;
+use warnings;
+
+use RT::Test tests => 34;
+
+my $lifecycles = RT->Config->Get('Lifecycles');
+$lifecycles->{foo} = {
+    initial  => ['initial'],
+    active   => ['open'],
+    inactive => ['resolved'],
+
+};
+
+RT::Lifecycle->FillCache();
+
+my $general = RT::Test->load_or_create_queue( Name => 'General' );
+my $foo = RT::Test->load_or_create_queue( Name => 'foo', Lifecycle => 'foo' );
+
+my $global_cf = RT::Test->load_or_create_custom_field(
+    Name  => 'global_cf',
+    Queue => 0,
+    Type  => 'FreeformSingle',
+);
+
+my $general_cf = RT::Test->load_or_create_custom_field(
+    Name  => 'general_cf',
+    Queue => 'General',
+    Type  => 'FreeformSingle',
+);
+
+my $foo_cf = RT::Test->load_or_create_custom_field(
+    Name  => 'foo_cf',
+    Queue => 'foo',
+    Type  => 'FreeformSingle'
+);
+
+my $root = RT::Test->load_or_create_user( Name => 'root', );
+my $user_a = RT::Test->load_or_create_user(
+    Name     => 'user_a',
+    Password => 'password',
+);
+my $user_b = RT::Test->load_or_create_user(
+    Name     => 'user_b',
+    Password => 'password',
+);
+
+ok(
+    RT::Test->set_rights(
+        {
+            Principal => $user_a,
+            Object    => $general,
+            Right     => ['OwnTicket'],
+        },
+        {
+            Principal => $user_b,
+            Object    => $foo,
+            Right     => ['OwnTicket'],
+        },
+    ),
+    'granted OwnTicket right for user_a and user_b'
+);
+
+my ( $url, $m ) = RT::Test->started_ok;
+ok( $m->login, 'logged in' );
+
+$m->get_ok( $url . '/Search/Build.html' );
+
+diag "check default statuses, cf and owners";
+my $form = $m->form_name('BuildQuery');
+ok( $form,                                     'found BuildQuery form' );
+ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf by default' );
+ok( !$form->find_input("ValueOf'CF.{general_cf}'"), 'no general_cf by default' );
+ok( !$form->find_input("ValueOf'CF.{foo_cf}'"), 'no foo_cf by default' );
+
+my $status_input = $form->find_input('ValueOfStatus');
+my @statuses     = sort $status_input->possible_values;
+is_deeply(
+    \@statuses, [ '', qw/initial new open rejected resolved stalled/], 'found all statuses'
+);
+
+my $owner_input = $form->find_input('ValueOfActor');
+my @owners     = sort $owner_input->possible_values;
+is_deeply(
+    \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users'
+);
+
+diag "limit queue to foo";
+$m->submit_form(
+    fields => { ValueOfQueue => 'foo' },
+    button => 'AddClause',
+);
+
+$form = $m->form_name('BuildQuery');
+ok( $form->find_input("ValueOf'CF.{foo_cf}'"), 'found foo_cf' );
+ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' );
+ok( !$form->find_input("ValueOf'CF.{general_cf}'"), 'still no general_cf' );
+$status_input = $form->find_input('ValueOfStatus');
+@statuses     = sort $status_input->possible_values;
+is_deeply(
+    \@statuses,
+    [ '', qw/initial open resolved/ ],
+    'found statuses from foo only'
+);
+
+$owner_input = $form->find_input('ValueOfActor');
+@owners     = sort $owner_input->possible_values;
+is_deeply(
+    \@owners, [ '', qw/Nobody root user_b/], 'no user_a'
+);
+
+diag "limit queue to general too";
+
+$m->submit_form(
+    fields => { ValueOfQueue => 'General' },
+    button => 'AddClause',
+);
+
+$form = $m->form_name('BuildQuery');
+ok( $form->find_input("ValueOf'CF.{general_cf}'"), 'found general_cf' );
+ok( $form->find_input("ValueOf'CF.{foo_cf}'"), 'found foo_cf' );
+ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' );
+$status_input = $form->find_input('ValueOfStatus');
+@statuses     = sort $status_input->possible_values;
+is_deeply(
+    \@statuses,
+    [ '', qw/initial new open rejected resolved stalled/ ],
+    'found all statuses again'
+);
+$owner_input = $form->find_input('ValueOfActor');
+@owners     = sort $owner_input->possible_values;
+is_deeply(
+    \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users again'
+);
+
+diag "limit queue to != foo";
+$m->get_ok( $url . '/Search/Build.html?NewQuery=1' );
+$m->submit_form(
+    form_name => 'BuildQuery',
+    fields => { ValueOfQueue => 'foo', QueueOp => '!=' },
+    button => 'AddClause',
+);
+
+$form = $m->form_name('BuildQuery');
+ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' );
+ok( !$form->find_input("ValueOf'CF.{foo_cf}'"), 'no foo_cf' );
+ok( !$form->find_input("ValueOf'CF.{general_cf}'"), 'no general_cf' );
+$status_input = $form->find_input('ValueOfStatus');
+@statuses     = sort $status_input->possible_values;
+is_deeply(
+    \@statuses, [ '', qw/initial new open rejected resolved stalled/],
+    'found all statuses'
+);
+$owner_input = $form->find_input('ValueOfActor');
+@owners     = sort $owner_input->possible_values;
+is_deeply(
+    \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users'
+);
+
+diag "limit queue to General OR foo";
+$m->get_ok( $url . '/Search/Edit.html' );
+$m->submit_form(
+    form_name => 'BuildQueryAdvanced',
+    fields => { Query => q{Queue = 'General' OR Queue = 'foo'} },
+);
+$form = $m->form_name('BuildQuery');
+ok( $form->find_input("ValueOf'CF.{general_cf}'"), 'found general_cf' );
+ok( $form->find_input("ValueOf'CF.{foo_cf}'"), 'found foo_cf' );
+ok( $form->find_input("ValueOf'CF.{global_cf}'"), 'found global_cf' );
+$status_input = $form->find_input('ValueOfStatus');
+@statuses     = sort $status_input->possible_values;
+is_deeply(
+    \@statuses,
+    [ '', qw/initial new open rejected resolved stalled/ ],
+    'found all statuses'
+);
+$owner_input = $form->find_input('ValueOfActor');
+@owners     = sort $owner_input->possible_values;
+is_deeply(
+    \@owners, [ '', qw/Nobody root user_a user_b/], 'found all users'
+);
diff --git a/rt/t/web/rest_cfs_with_same_name.t b/rt/t/web/rest_cfs_with_same_name.t
new file mode 100644 (file)
index 0000000..958f671
--- /dev/null
@@ -0,0 +1,88 @@
+use strict;
+use warnings;
+use RT::Interface::REST;
+
+use RT::Test tests => 25;
+
+my ( $baseurl, $m ) = RT::Test->started_ok;
+for my $queue_name (qw/foo bar/) {
+
+    my $queue = RT::Test->load_or_create_queue( Name => $queue_name );
+    ok( $queue, "created queue $queue_name" );
+    my $cf = RT::Test->load_or_create_custom_field(
+        Name  => 'test',
+        Type  => 'Freeform',
+        Queue => $queue_name,
+    );
+    ok( $cf->id, "created cf test for queue $queue_name " . $cf->id );
+
+    $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.{test}: baz";
+
+    $text = join "\n", @lines;
+
+    ok( $text =~ s/Subject:\s*$/Subject: test cf/m,
+        "successfully replaced subject" );
+    ok( $text =~ s/Queue: General\s*$/Queue: $queue_name/m,
+        "successfully replaced Queue" );
+
+    $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, "test cf", "subject successfully set" );
+    is( $ticket->Queue, $queue->id, "queue successfully set" );
+    is( $ticket->FirstCustomFieldValue("test"), "baz", "cf successfully set" );
+
+    $m->post(
+        "$baseurl/REST/1.0/ticket/show",
+        [
+            user   => 'root',
+            pass   => 'password',
+            format => 'l',
+            id     => "ticket/$id",
+        ]
+    );
+    $text = $m->content;
+    like( $text, qr/^CF\.{test}: baz\s*$/m, 'cf value in rest show' );
+
+    $text =~ s{.*}{}; # remove header
+    $text =~ s!CF\.{test}: baz!CF.{test}: newbaz!;
+    $m->post(
+        "$baseurl/REST/1.0/ticket/edit",
+        [
+            user => 'root',
+            pass => 'password',
+            content => $text,
+        ],
+        Content_Type => 'form-data'
+    );
+    $m->content =~ /Ticket ($id) updated/;
+    is( $ticket->FirstCustomFieldValue("test"), "newbaz", "cf successfully updated" );
+}
+