]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1069799 - move the QA repository into the main repository
authorDavid Lawrence <dkl@mozilla.com>
Fri, 26 Feb 2016 17:57:55 +0000 (17:57 +0000)
committerDavid Lawrence <dkl@mozilla.com>
Fri, 26 Feb 2016 17:57:55 +0000 (17:57 +0000)
r=LpSolit

97 files changed:
docker_files/runtests.sh
t/Support/Files.pm
xt/README [deleted file]
xt/config/checksetup_answers.txt [new file with mode: 0644]
xt/config/generate_test_data.pl [new file with mode: 0755]
xt/config/patch.diff [new file with mode: 0644]
xt/config/selenium_test.conf [new file with mode: 0644]
xt/extensions/QA/Config.pm [new file with mode: 0644]
xt/extensions/QA/Extension.pm [new file with mode: 0644]
xt/extensions/QA/lib/Util.pm [new file with mode: 0644]
xt/extensions/QA/template/en/default/hook/README [new file with mode: 0644]
xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl [new file with mode: 0644]
xt/extensions/QA/template/en/default/qa/README [new file with mode: 0644]
xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl [new file with mode: 0644]
xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl [new file with mode: 0644]
xt/extensions/QA/template/en/default/qa/results.html.tmpl [new file with mode: 0644]
xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl [new file with mode: 0644]
xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl [new file with mode: 0644]
xt/extensions/QA/web/README [new file with mode: 0644]
xt/lib/Bugzilla/Test/Search.pm [deleted file]
xt/lib/Bugzilla/Test/Search/AndTest.pm [deleted file]
xt/lib/Bugzilla/Test/Search/Constants.pm [deleted file]
xt/lib/Bugzilla/Test/Search/CustomTest.pm [deleted file]
xt/lib/Bugzilla/Test/Search/FieldTest.pm [deleted file]
xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm [deleted file]
xt/lib/Bugzilla/Test/Search/InjectionTest.pm [deleted file]
xt/lib/Bugzilla/Test/Search/NotTest.pm [deleted file]
xt/lib/Bugzilla/Test/Search/OperatorTest.pm [deleted file]
xt/lib/Bugzilla/Test/Search/OrTest.pm [deleted file]
xt/lib/QA/REST.pm [new file with mode: 0644]
xt/lib/QA/RPC.pm [new file with mode: 0644]
xt/lib/QA/RPC/JSONRPC.pm [new file with mode: 0644]
xt/lib/QA/RPC/XMLRPC.pm [new file with mode: 0644]
xt/lib/QA/Tests.pm [new file with mode: 0644]
xt/lib/QA/Util.pm [new file with mode: 0644]
xt/rest/bugzilla.t [new file with mode: 0644]
xt/rest/classification.t [new file with mode: 0644]
xt/search.t [deleted file]
xt/selenium/bug_edit.t [new file with mode: 0644]
xt/selenium/choose_priority.t [new file with mode: 0644]
xt/selenium/classifications.t [new file with mode: 0644]
xt/selenium/config.t [new file with mode: 0644]
xt/selenium/create_user_accounts.t [new file with mode: 0644]
xt/selenium/custom_fields.t [new file with mode: 0644]
xt/selenium/custom_fields_admin.t [new file with mode: 0644]
xt/selenium/dependencies.t [new file with mode: 0644]
xt/selenium/edit_products_properties.t [new file with mode: 0644]
xt/selenium/email_preferences.t [new file with mode: 0644]
xt/selenium/enter_new_bug.t [new file with mode: 0644]
xt/selenium/flags.t [new file with mode: 0644]
xt/selenium/flags2.t [new file with mode: 0644]
xt/selenium/groups.t [new file with mode: 0644]
xt/selenium/keywords.t [new file with mode: 0644]
xt/selenium/login.t [new file with mode: 0644]
xt/selenium/milestones.t [new file with mode: 0644]
xt/selenium/password_complexity.t [new file with mode: 0644]
xt/selenium/private_attachments.t [new file with mode: 0644]
xt/selenium/qa_contact.t [new file with mode: 0644]
xt/selenium/require_login.t [new file with mode: 0644]
xt/selenium/sanity_check.t [new file with mode: 0644]
xt/selenium/saved_searches.t [new file with mode: 0644]
xt/selenium/search.t [new file with mode: 0644]
xt/selenium/security.t [new file with mode: 0644]
xt/selenium/shared_searches.t [new file with mode: 0644]
xt/selenium/show_all_products.t [new file with mode: 0644]
xt/selenium/shutdown.t [new file with mode: 0644]
xt/selenium/status_whiteboard.t [new file with mode: 0644]
xt/selenium/strict_isolation.t [new file with mode: 0644]
xt/selenium/sudo_sessions.t [new file with mode: 0644]
xt/selenium/target_milestones.t [new file with mode: 0644]
xt/selenium/time_summary.t [new file with mode: 0644]
xt/selenium/user_groups.t [new file with mode: 0644]
xt/selenium/user_matching.t [new file with mode: 0644]
xt/selenium/user_preferences.t [new file with mode: 0644]
xt/selenium/user_privs.t [new file with mode: 0644]
xt/selenium/votes.t [new file with mode: 0644]
xt/webservice/bug_add_attachment.t [new file with mode: 0644]
xt/webservice/bug_add_comment.t [new file with mode: 0644]
xt/webservice/bug_attachments.t [new file with mode: 0644]
xt/webservice/bug_comments.t [new file with mode: 0644]
xt/webservice/bug_create.t [new file with mode: 0644]
xt/webservice/bug_fields.t [new file with mode: 0644]
xt/webservice/bug_get.t [new file with mode: 0644]
xt/webservice/bug_history.t [new file with mode: 0644]
xt/webservice/bug_legal_values.t [new file with mode: 0644]
xt/webservice/bug_search.t [new file with mode: 0644]
xt/webservice/bug_update.t [new file with mode: 0644]
xt/webservice/bug_update_see_also.t [new file with mode: 0644]
xt/webservice/bugzilla.t [new file with mode: 0644]
xt/webservice/group_create.t [new file with mode: 0644]
xt/webservice/jsonp.t [new file with mode: 0644]
xt/webservice/product_create.t [new file with mode: 0644]
xt/webservice/product_get.t [new file with mode: 0644]
xt/webservice/user_create.t [new file with mode: 0644]
xt/webservice/user_get.t [new file with mode: 0644]
xt/webservice/user_login_logout.t [new file with mode: 0644]
xt/webservice/user_offer_account_by_email.t [new file with mode: 0644]

index c15dca9ee8c94ac9a6e6a5e9f03745e8de3f9de1..04152189f1b00a356bf2dd8b10b5e4d63a8f90a2 100755 (executable)
@@ -41,11 +41,6 @@ if [ "$TEST_SUITE" = "docs" ]; then
     exit $?
 fi
 
-echo -e "\n== Cloning QA test suite"
-cd $BUGZILLA_ROOT
-echo "Cloning git repo $GITHUB_QA_GIT branch $GITHUB_BASE_BRANCH ..."
-git clone $GITHUB_QA_GIT -b $GITHUB_BASE_BRANCH qa
-
 echo -e "\n== Starting database"
 /usr/bin/mysqld_safe &
 sleep 3
@@ -55,32 +50,24 @@ echo -e "\n== Starting memcached"
 sleep 3
 
 echo -e "\n== Updating configuration"
-sed -e "s?%DB%?$BUGS_DB_DRIVER?g" --in-place qa/config/checksetup_answers.txt
-sed -e "s?%DB_NAME%?bugs_test?g" --in-place qa/config/checksetup_answers.txt
-sed -e "s?%USER%?$USER?g" --in-place qa/config/checksetup_answers.txt
-sed -e "s?%TRAVIS_BUILD_DIR%?$BUGZILLA_ROOT?g" --in-place qa/config/selenium_test.conf
-echo "\$answer{'memcached_servers'} = 'localhost:11211';" >> qa/config/checksetup_answers.txt
-
-if [ "$TEST_SUITE" == "checksetup" ]; then
-    cd $BUGZILLA_ROOT/qa
-    /bin/bash /docker_files/buildbot_step "Checksetup" ./test_checksetup.pl config/config-checksetup-$BUGS_DB_DRIVER
-    exit $?
-fi
+sed -e "s?%DB%?$BUGS_DB_DRIVER?g" --in-place xt/config/checksetup_answers.txt
+echo "\$answer{'memcached_servers'} = 'localhost:11211';" >> xt/config/checksetup_answers.txt
 
 echo -e "\n== Running checksetup"
 cd $BUGZILLA_ROOT
-./checksetup.pl qa/config/checksetup_answers.txt
-./checksetup.pl qa/config/checksetup_answers.txt
+./checksetup.pl xt/config/checksetup_answers.txt
+./checksetup.pl xt/config/checksetup_answers.txt
 
 echo -e "\n== Generating test data"
-cd $BUGZILLA_ROOT/qa/config
-perl -I../../local/lib/perl5 generate_test_data.pl
+cd $BUGZILLA_ROOT/xt/config
+perl generate_test_data.pl
 
 echo -e "\n== Starting web server"
 sed -e "s?^#Perl?Perl?" --in-place /etc/httpd/conf.d/bugzilla.conf
 /usr/sbin/httpd &
 sleep 3
 
+cd $BUGZILLA_ROOT
 if [ "$TEST_SUITE" = "selenium" ]; then
     export DISPLAY=:0
 
@@ -100,13 +87,11 @@ if [ "$TEST_SUITE" = "selenium" ]; then
     # but no tests actually executed.
     [ $NO_TESTS ] && exit 0
 
-    cd $BUGZILLA_ROOT/qa/t
-    /bin/bash /docker_files/buildbot_step "Selenium" prove -f -v -I$BUGZILLA_ROOT/lib test_*.t
+    /bin/bash /docker_files/buildbot_step "Selenium" prove -f -v xt/selenium/*.t
     exit $?
 fi
 
 if [ "$TEST_SUITE" = "webservices" ]; then
-    cd $BUGZILLA_ROOT/qa/t
-    /bin/bash /docker_files/buildbot_step "Webservices" prove -f -v -I$BUGZILLA_ROOT/lib {rest,webservice}_*.t
+    /bin/bash /docker_files/buildbot_step "Webservices" prove -f -v xt/{rest,webservice}/*.t
     exit $?
 fi
index 39bacccfc36e41a68fa889caefa5fd5d99e2b8fc..e06cda7386a1263b4cac5c992a739093fe1bce08 100644 (file)
@@ -28,7 +28,7 @@ foreach my $extension (@extensions) {
     find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, $extension);
 }
 
-our @test_files = glob('t/*.t');
+our @test_files = glob('t/*.t xt/*/*.t');
 
 sub isTestingFile {
     my ($file) = @_;
diff --git a/xt/README b/xt/README
deleted file mode 100644 (file)
index 22f9f17..0000000
--- a/xt/README
+++ /dev/null
@@ -1,18 +0,0 @@
-The tests in this directory require a working database, as opposed
-to the tests in t/, which simply test the code without a working
-installation.
-
-Some of the tests may modify your current working installation, even
-if only temporarily. To run the tests that modify your database,
-set the environment variable BZ_WRITE_TESTS to 1.
-
-Some tests also take additional, optional arguments. You can pass arguments
-to tests like:
-
-  prove xt/search.t :: --long --operators=equals,notequals
-
-Note the "::"--that is necessary to note that the arguments are going to
-the test, not to "prove".
-
-See the perldoc of the individual tests to see what options they support,
-or do "perl xt/search.t --help".
diff --git a/xt/config/checksetup_answers.txt b/xt/config/checksetup_answers.txt
new file mode 100644 (file)
index 0000000..54473d3
--- /dev/null
@@ -0,0 +1,25 @@
+ $answer{'db_host'} = 'localhost';
+ $answer{'db_driver'} = '%DB%';
+ $answer{'db_port'} = 0;
+ $answer{'db_name'} = 'bugs_test',
+ $answer{'db_user'} = 'bugs';
+ $answer{'db_pass'} = 'bugs';
+ $answer{'db_sock'} = '';
+ $answer{'db_check'} = 1;
+ $answer{'db_mysql_ssl_ca_file'} = '';
+ $answer{'db_mysql_ssl_ca_path'} = '';
+ $answer{'db_mysql_ssl_client_cert'} = '';
+ $answer{'db_mysql_ssl_client_key'} = '';
+ $answer{'urlbase'} = 'http://localhost/bugzilla/';
+ $answer{'create_htaccess'} = '';
+ $answer{'use_suexec'} = '';
+ $answer{'index_html'} = 0;
+ $answer{'cvsbin'} = '/usr/bin/cvs';
+ $answer{'interdiffbin'} = '/usr/bin/interdiff';
+ $answer{'diffpath'} = '/usr/bin';
+ $answer{'webservergroup'} = 'bugzilla';
+ $answer{'ADMIN_OK'} = 'Y';
+ $answer{'ADMIN_EMAIL'} = 'admin@bugzilla.org';
+ $answer{'ADMIN_PASSWORD'} = 'password';
+ $answer{'ADMIN_REALNAME'} = 'QA Admin';
+ $answer{'NO_PAUSE'} = 1;
diff --git a/xt/config/generate_test_data.pl b/xt/config/generate_test_data.pl
new file mode 100755 (executable)
index 0000000..ab18669
--- /dev/null
@@ -0,0 +1,616 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Cwd;
+
+my $conf_path;
+my $config;
+
+BEGIN {
+    say 'reading the config file...';
+    my $conf_file = 'selenium_test.conf';
+    $config = do "$conf_file"
+        or die "can't read configuration '$conf_file': $!$@";
+
+    $conf_path = $config->{bugzilla_path};
+
+    # We don't want randomly-generated keys. We want the ones specified
+    # in the config file so that we can use them in tests scripts.
+    *Bugzilla::User::APIKey::_check_api_key = sub { return $_[1]; };
+}
+
+use lib $conf_path;
+
+use Bugzilla;
+use Bugzilla::Attachment;
+use Bugzilla::Bug;
+use Bugzilla::User;
+use Bugzilla::Install;
+use Bugzilla::Milestone;
+use Bugzilla::Product;
+use Bugzilla::Component;
+use Bugzilla::Group;
+use Bugzilla::Version;
+use Bugzilla::Constants;
+use Bugzilla::Keyword;
+use Bugzilla::Config qw(:admin);
+use Bugzilla::User::Setting;
+use Bugzilla::User::APIKey;
+
+my $dbh = Bugzilla->dbh;
+
+# set Bugzilla usage mode to USAGE_MODE_CMDLINE
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+##########################################################################
+# Set Parameters
+##########################################################################
+
+# Some parameters must be turned on to create bugs requiring them.
+# They are also expected to be turned on by some webservice_*.t scripts.
+my ($urlbase, $sslbase);
+$urlbase = $config->{browser_url} . '/' . $config->{bugzilla_installation};
+$urlbase .= '/' unless $urlbase =~ /\/$/;
+
+if ($urlbase =~ /^https/) {
+    $sslbase = $urlbase;
+    $urlbase =~ s/^https(.+)$/http$1/;
+}
+
+my %set_params = (
+    urlbase => $urlbase,
+    sslbase => $sslbase,
+    useqacontact  => 1,
+    mail_delivery_method => 'Test',
+    maxattachmentsize => 256,
+);
+
+my $params_modified;
+foreach my $param (keys %set_params) {
+    my $value = $set_params{$param};
+    next unless defined $value && Bugzilla->params->{$param} ne $value;
+    SetParam($param, $value);
+    $params_modified = 1;
+}
+
+write_params() if $params_modified;
+
+##########################################################################
+# Set Default User Preferences
+##########################################################################
+
+# When editing a bug, the page being displayed depends on the
+# post_bug_submit_action user pref. We set it globally so that we know
+# the exact behavior of process_bug.cgi.
+my %user_prefs = (post_bug_submit_action => 'nothing');
+
+foreach my $pref (keys %user_prefs) {
+    my $value = $user_prefs{$pref};
+    Bugzilla::User::Setting::set_default($pref, $value, 0);
+}
+
+##########################################################################
+# Create Users
+##########################################################################
+
+# First of all, remove the default .* regexp for the editbugs group.
+my $group = Bugzilla::Group->new({ name => 'editbugs' });
+$group->set_user_regexp('');
+$group->update();
+
+my @usernames = (
+    'admin',            'no-privs',
+    'QA-Selenium-TEST', 'canconfirm',
+    'tweakparams',      'permanent_user',
+    'editbugs',         'disabled',
+);
+
+say 'creating user accounts...';
+foreach my $username (@usernames) {
+    my ($password, $login);
+
+    my $prefix = $username;
+    if ($username eq 'permanent_user') {
+        $password = $config->{admin_user_passwd};
+        $login = $config->{$username};
+    }
+    elsif ($username eq 'no-privs') {
+        $prefix = 'unprivileged';
+    }
+    elsif ($username eq 'QA-Selenium-TEST') {
+        $prefix = 'QA_Selenium_TEST';
+    }
+
+    $password ||= $config->{"${prefix}_user_passwd"};
+    $login ||= $config->{"${prefix}_user_login"};
+    my $api_key = $config->{"${prefix}_user_api_key"};
+
+    if (is_available_username($login)) {
+        my %extra_args;
+        if ($username eq 'disabled') {
+            $extra_args{disabledtext} = '!!This is the text!!';
+        }
+
+        my $user = Bugzilla::User->create(
+            { login_name    => $login,
+              realname      => $username,
+              cryptpassword => $password,
+              %extra_args,
+            }
+        );
+
+        if ($api_key) {
+            Bugzilla::User::APIKey->create(
+                { user_id     => $user->id,
+                  description => 'API key for QA tests',
+                  api_key     => $api_key }
+            );
+        }
+
+        if ($username eq 'admin' or $username eq 'permanent_user') {
+            Bugzilla::Install::make_admin($login);
+        }
+    }
+}
+
+##########################################################################
+# Create Bugs
+##########################################################################
+
+# login to bugzilla
+my $admin_user = Bugzilla::User->check($config->{admin_user_login});
+Bugzilla->set_user($admin_user);
+
+my %field_values = (
+    'priority'     => 'Highest',
+    'bug_status'   => 'CONFIRMED',
+    'version'      => 'unspecified',
+    'bug_file_loc' => '',
+    'comment'      => 'please ignore this bug',
+    'component'    => 'TestComponent',
+    'rep_platform' => 'All',
+    'short_desc'   => 'This is a testing bug only',
+    'product'      => 'TestProduct',
+    'op_sys'       => 'Linux',
+    'bug_severity' => 'normal',
+);
+
+say 'creating bugs...';
+my $bug = Bugzilla::Bug->create( \%field_values );
+say 'Bug ' . $bug->id . ' created';
+if (Bugzilla::Bug->new('public_bug')->{error}) {
+    # The deadline must be set so that this bug can be used to test
+    # timetracking fields using WebServices.
+    $bug = Bugzilla::Bug->create({ %field_values, alias => 'public_bug', deadline => '2010-01-01' });
+    say 'Bug ' . $bug->id . ' (alias: public_bug) created';
+}
+
+##########################################################################
+# Create Classifications
+##########################################################################
+
+my @classifications = ({ name        => 'Class2_QA',
+                         description => "required by Selenium... DON'T DELETE" },
+);
+
+say 'creating classifications...';
+for my $class (@classifications) {
+    my $new_class = Bugzilla::Classification->new({ name => $class->{name} });
+    if (!$new_class) {
+        $dbh->do('INSERT INTO classifications (name, description) VALUES (?, ?)',
+                 undef, ($class->{name}, $class->{description}));
+    }
+}
+##########################################################################
+# Create Products
+##########################################################################
+
+my @products = (
+    {   product_name     => 'QA-Selenium-TEST',
+        description      => "used by Selenium test.. DON'T DELETE",
+        versions         => ['unspecified', 'QAVersion'],
+        milestones       => ['QAMilestone'],
+        defaultmilestone => '---',
+        components       => [
+            {   name             => 'QA-Selenium-TEST',
+                description      => "used by Selenium test.. DON'T DELETE",
+                initialowner     => $config->{QA_Selenium_TEST_user_login},
+                initialqacontact => $config->{QA_Selenium_TEST_user_login},
+                initial_cc       => [$config->{QA_Selenium_TEST_user_login}],
+            }
+        ],
+    },
+
+    {   product_name => 'Another Product',
+        description  => 'Alternate product used by Selenium. <b>Do not edit!</b>',
+        versions         => ['unspecified', 'Another1', 'Another2'],
+        milestones       => ['AnotherMS1', 'AnotherMS2', 'Milestone'],
+        defaultmilestone => '---',
+        components       => [
+            {   name             => 'c1',
+                description      => 'c1',
+                initialowner     => $config->{permanent_user},
+                initialqacontact => '',
+                initial_cc       => [],
+            },
+            {   name             => 'c2',
+                description      => 'c2',
+                initialowner     => $config->{permanent_user},
+                initialqacontact => '',
+                initial_cc       => [],
+            },
+        ],
+    },
+
+    {   product_name     => 'C2 Forever',
+        description      => 'I must remain in the Class2_QA classification ' .
+                            'in all cases! Do not edit!',
+        classification   => 'Class2_QA',
+        versions         => ['unspecified', 'C2Ver'],
+        milestones       => ['C2Mil'],
+        defaultmilestone => '---',
+        components       => [
+            {   name             => 'Helium',
+                description      => 'Feel free to add bugs to me',
+                initialowner     => $config->{permanent_user},
+                initialqacontact => '',
+                initial_cc       => [],
+            }
+        ],
+    },
+
+    {   product_name     => 'QA Entry Only',
+        description      => 'Only the QA group may enter bugs here.',
+        versions         => ['unspecified'],
+        milestones       => [],
+        defaultmilestone => '---',
+        components       => [
+            {   name             => 'c1',
+                description      => "Same name as Another Product's component",
+                initialowner     => $config->{QA_Selenium_TEST_user_login},
+                initialqacontact => '',
+                initial_cc       => [],
+            }
+        ],
+    },
+
+    {   product_name     => 'QA Search Only',
+        description      => 'Only the QA group may search for bugs here.',
+        versions         => ['unspecified'],
+        milestones       => [],
+        defaultmilestone => '---',
+        components       => [
+            {   name             => 'c1',
+                description      => 'Still same name as the Another component',
+                initialowner     => $config->{QA_Selenium_TEST_user_login},
+                initialqacontact => '',
+                initial_cc       => [],
+            }
+        ],
+    },
+);
+
+say 'creating products...';
+foreach my $product (@products) {
+    my $new_product = Bugzilla::Product->new({ name => $product->{product_name} });
+    if (!$new_product) {
+        my $class_id = 1;
+        if ($product->{classification}) {
+            $class_id = Bugzilla::Classification->new({ name => $product->{classification} })->id;
+        }
+        $dbh->do('INSERT INTO products (name, description, classification_id) VALUES (?, ?, ?)',
+            undef, ($product->{product_name}, $product->{description}, $class_id));
+
+        $new_product = Bugzilla::Product->new({ name => $product->{product_name} });
+
+        $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?, ?)',
+            undef, ($new_product->id, $product->{defaultmilestone} ));
+
+        # Now clear the internal list of accessible products.
+        delete Bugzilla->user->{selectable_products};
+
+        foreach my $component (@{ $product->{components} }) {
+            Bugzilla::Component->create(
+                {   name             => $component->{name},
+                    product          => $new_product,
+                    description      => $component->{description},
+                    initialowner     => $component->{initialowner},
+                    initialqacontact => $component->{initialqacontact},
+                    initial_cc       => $component->{initial_cc},
+                }
+            );
+        }
+    }
+
+    foreach my $version (@{ $product->{versions} }) {
+        my $new_version = Bugzilla::Version->new({ name => $version, product => $new_product });
+        if (!$new_version) {
+            Bugzilla::Version->create({ value => $version, product => $new_product });
+        }
+    }
+
+    foreach my $milestone (@{ $product->{milestones} }) {
+        my $new_milestone = Bugzilla::Milestone->new({ name => $milestone, product => $new_product });
+        if (!$new_milestone) {
+            # We don't use Bugzilla::Milestone->create because we want to
+            # bypass security checks.
+            $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?,?)',
+                     undef, $new_product->id, $milestone);
+        }
+    }
+}
+
+##########################################################################
+# Create Groups
+##########################################################################
+
+# create Master group
+my ($group_name, $group_desc) = ('Master', 'Master Selenium Group <b>DO NOT EDIT!</b>');
+
+say 'creating groups...';
+my $new_group = Bugzilla::Group->new({ name => $group_name });
+if (!$new_group) {
+    my $group = Bugzilla::Group->create({ name => $group_name,
+                                          description => $group_desc,
+                                          isbuggroup => 1});
+
+    $dbh->do('INSERT INTO group_control_map
+              (group_id, product_id, entry, membercontrol, othercontrol, canedit)
+              SELECT ?, products.id, 0, ?, ?, 0 FROM products',
+              undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPSHOWN));
+}
+
+# create QA-Selenium-TEST group. Do not use Group->create() so that
+# the admin group doesn't inherit membership (yes, that's what we want!).
+($group_name, $group_desc) = ('QA-Selenium-TEST', "used by Selenium test.. DON'T DELETE");
+
+$new_group = Bugzilla::Group->new({ name => $group_name });
+if (!$new_group) {
+    $dbh->do('INSERT INTO groups (name, description, isbuggroup, isactive)
+              VALUES (?, ?, 1, 1)', undef, ($group_name, $group_desc));
+}
+
+##########################################################################
+# Add Users to Groups
+##########################################################################
+
+my @users_groups = (
+    { user => $config->{QA_Selenium_TEST_user_login}, group => 'QA-Selenium-TEST' },
+    { user => $config->{tweakparams_user_login},      group => 'tweakparams' },
+    { user => $config->{canconfirm_user_login},       group => 'canconfirm' },
+    { user => $config->{editbugs_user_login},         group => 'editbugs' },
+);
+
+say 'adding users to groups...';
+foreach my $user_group (@users_groups) {
+    my $group = Bugzilla::Group->new({ name => $user_group->{group} });
+    my $user = Bugzilla::User->new({ name => $user_group->{user} });
+
+    my $sth_add_mapping =
+      $dbh->prepare('INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
+                     VALUES (?, ?, ?, ?)');
+    # Don't crash if the entry already exists.
+    eval { $sth_add_mapping->execute($user->id, $group->id, 0, GRANT_DIRECT); };
+}
+
+##########################################################################
+# Associate Products with groups
+##########################################################################
+
+# Associate the QA-Selenium-TEST group with the QA-Selenium-TEST.
+my $created_group   = Bugzilla::Group->new({ name => 'QA-Selenium-TEST' });
+my $secret_product = Bugzilla::Product->new({ name => 'QA-Selenium-TEST' });
+my $no_entry = Bugzilla::Product->new({ name => 'QA Entry Only' });
+my $no_search = Bugzilla::Product->new({ name => 'QA Search Only' });
+
+say 'restricting products to groups...';
+# Don't crash if the entries already exist.
+my $sth = $dbh->prepare('INSERT INTO group_control_map
+                         (group_id, product_id, entry, membercontrol, othercontrol, canedit)
+                         VALUES (?, ?, ?, ?, ?, ?)');
+eval { $sth->execute($created_group->id, $secret_product->id, 1, CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0); };
+eval { $sth->execute($created_group->id, $no_entry->id,       1, CONTROLMAPNA,        CONTROLMAPNA,        0); };
+eval { $sth->execute($created_group->id, $no_search->id,      0, CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0); };
+
+##########################################################################
+# Create flag types
+##########################################################################
+
+my @flagtypes = (
+    {name => 'spec_multi_flag', desc => 'Specifically requestable and multiplicable bug flag',
+     is_requestable => 1, is_requesteeble => 1, is_multiplicable => 1, grant_group => 'editbugs',
+     target_type => 'b', cc_list => '', inclusions => ['Another Product:c1']},
+);
+
+say 'creating flag types...';
+foreach my $flag (@flagtypes) {
+    # The name is not unique, even within a single product/component, so there is NO WAY
+    # to know if the existing flag type is the one we want or not.
+    # As our Selenium scripts would be confused anyway if there is already such a flag name,
+    # we simply skip it and assume the existing flag type is the one we want.
+    next if Bugzilla::FlagType->new({ name => $flag->{name} });
+
+    my $grant_group_id = $flag->{grant_group} ? Bugzilla::Group->new({ name => $flag->{grant_group} })->id : undef;
+    my $request_group_id = $flag->{request_group} ? Bugzilla::Group->new({ name => $flag->{request_group} })->id : undef;
+
+    $dbh->do('INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable,
+                                     is_requesteeble, is_multiplicable, grant_group_id, request_group_id)
+                             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
+             undef, ($flag->{name}, $flag->{desc}, $flag->{cc_list}, $flag->{target_type},
+                     $flag->{is_requestable}, $flag->{is_requesteeble}, $flag->{is_multiplicable},
+                     $grant_group_id, $request_group_id));
+
+    my $type_id = $dbh->bz_last_key('flagtypes', 'id');
+
+    foreach my $inclusion (@{$flag->{inclusions}}) {
+        my ($product, $component) = split(':', $inclusion);
+        my ($prod_id, $comp_id);
+        if ($product) {
+            my $prod_obj = Bugzilla::Product->new({ name => $product });
+            $prod_id = $prod_obj->id;
+            if ($component) {
+                $comp_id = Bugzilla::Component->new({ name => $component, product => $prod_obj})->id;
+            }
+        }
+        $dbh->do('INSERT INTO flaginclusions (type_id, product_id, component_id)
+                  VALUES (?, ?, ?)',
+                 undef, ($type_id, $prod_id, $comp_id));
+    }
+}
+
+##########################################################################
+# Create custom fields
+##########################################################################
+
+my @fields = (
+    { name        => 'cf_QA_status',
+      description => 'QA Status',
+      type        => FIELD_TYPE_MULTI_SELECT,
+      sortkey     => 100,
+      mailhead    => 0,
+      enter_bug   => 1,
+      obsolete    => 0,
+      custom      => 1,
+      values      => ['verified', 'in progress', 'untested']
+    },
+    { name        => 'cf_single_select',
+      description => 'SingSel',
+      type        => FIELD_TYPE_SINGLE_SELECT,
+      mailhead    => 0,
+      enter_bug   => 1,
+      custom      => 1,
+      values      => [qw(one two three)],
+    },
+);
+
+say 'creating custom fields...';
+foreach my $f (@fields) {
+    # Skip existing custom fields.
+    next if Bugzilla::Field->new({ name => $f->{name} });
+
+    my @values;
+    if (exists $f->{values}) {
+        @values = @{$f->{values}};
+        # We have to delete this key, else create() will complain
+        # that 'values' is not an existing column name.
+        delete $f->{values};
+    }
+    my $field = Bugzilla::Field->create($f);
+
+    # Now populate the table with valid values, if necessary.
+    next unless scalar @values;
+
+    my $sth = $dbh->prepare('INSERT INTO ' . $field->name . ' (value) VALUES (?)');
+    foreach my $value (@values) {
+        $sth->execute($value);
+    }
+}
+
+####################################################################
+# Set Parameters That Require Other Things To Have Been Done First #
+####################################################################
+
+if (Bugzilla->params->{insidergroup} ne 'QA-Selenium-TEST') {
+    SetParam('insidergroup', 'QA-Selenium-TEST');
+    write_params();
+}
+
+if (Bugzilla->params->{timetrackinggroup} ne 'editbugs') {
+    SetParam('timetrackinggroup', 'editbugs');
+    write_params();
+}
+
+########################
+# Create a Private Bug #
+########################
+
+my $test_user = Bugzilla::User->check($config->{QA_Selenium_TEST_user_login});
+$test_user->{'groups'} = [
+    Bugzilla::Group->new({ name => 'editbugs' }),
+    Bugzilla::Group->new({ name => 'QA-Selenium-TEST' })
+]; # editbugs is needed for alias creation
+Bugzilla->set_user($test_user);
+
+if (Bugzilla::Bug->new('private_bug')->{error}) {
+    say 'Creating private bug...';
+    my %priv_values = %field_values;
+    $priv_values{alias} = 'private_bug';
+    $priv_values{product} = 'QA-Selenium-TEST';
+    $priv_values{component} = 'QA-Selenium-TEST';
+    my $bug = Bugzilla::Bug->create(\%priv_values);
+    say 'Bug ' . $bug->id . ' (alias: private_bug) created';
+}
+
+######################
+# Create Attachments #
+######################
+
+say 'creating attachments...';
+# We use the contents of this script as the attachment.
+open(my $attachment_fh, '<', __FILE__) or die __FILE__ . ": $!";
+my $attachment_contents;
+{
+    local $/;
+    $attachment_contents = <$attachment_fh>;
+}
+close($attachment_fh);
+
+foreach my $alias (qw(public_bug private_bug)) {
+    my $bug = Bugzilla::Bug->new($alias);
+
+    foreach my $is_private (0, 1) {
+        Bugzilla::Attachment->create({
+            bug  => $bug,
+            data => $attachment_contents,
+            description => "${alias}_${is_private}",
+            filename  => "${alias}_${is_private}.pl",
+            mimetype  => 'application/x-perl',
+            isprivate => $is_private,
+        });
+    }
+}
+
+###################
+# Create Keywords #
+###################
+
+my @keywords = (
+    { name => 'test-keyword-1',
+      description => 'Created for Bugzilla QA Tests, Keyword 1' },
+    { name => 'test-keyword-2',
+      description => 'Created for Bugzilla QA Tests, Keyword 2' },
+);
+
+say 'creating keywords...';
+foreach my $kw (@keywords) {
+    next if Bugzilla::Keyword->new({ name => $kw->{name} });
+    Bugzilla::Keyword->create($kw);
+}
+
+############################
+# Install the QA extension #
+############################
+
+say 'copying the QA extension...';
+my $output = `cp -R ../extensions/QA $conf_path/extensions/.`;
+print $output if $output;
+
+my $cwd = cwd();
+chdir($conf_path);
+$output = `perl contrib/fixperms.pl`;
+print $output if $output;
+chdir($cwd);
+
+say 'installation and configuration complete!';
diff --git a/xt/config/patch.diff b/xt/config/patch.diff
new file mode 100644 (file)
index 0000000..cbaff02
--- /dev/null
@@ -0,0 +1,18 @@
+Index: Bugzilla/Config/MTA.pm
+===================================================================
+RCS file: /cvsroot/mozilla/webtools/bugzilla/Bugzilla/Config/MTA.pm,v
+retrieving revision 1.13
+diff -3 -p -u -r1.13 MTA.pm
+--- Bugzilla/Config/MTA.pm 13 Nov 2006 23:32:28 -0000 1.13
++++ Bugzilla/Config/MTA.pm  9 Dec 2006 12:19:44 -0000
+@@ -44,7 +44,9 @@ sub get_param_list {
+   {
+    name => 'mail_delivery_method',
+    type => 's',
+-   choices => [Email::Send->new()->all_mailers(), 'None'],
++   # Bugzilla is not ready yet to send mails to newsgroups, and 'IO'
++   # is of no use for now as we already have our own 'Test' mode.
++   choices => [grep {$_ ne 'NNTP' && $_ ne 'IO'} Email::Send->new()->all_mailers(), 'None'],
+    default => 'Sendmail',
+    checker => \&check_mail_delivery_method
+   },
diff --git a/xt/config/selenium_test.conf b/xt/config/selenium_test.conf
new file mode 100644 (file)
index 0000000..e4d7e44
--- /dev/null
@@ -0,0 +1,46 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# To make this configuration file useful to you, you have to:
+# - set the path and URL to your Bugzilla installation.
+# - replace @my.company by something more relevant to you,
+#   also what comes before @my.company if you want/need to.
+# - set passwords for each user accounts.
+
+{   'browser'                           => '*firefox',
+    'experimental_browser_launcher'     => '*chrome',
+    'host'                              => 'localhost',
+    'port'                              => 4444,
+    'browser_url'                       => 'http://localhost',
+    'attachment_file'                   => '/home/bugzilla/devel/htdocs/bugzilla/xt/config/patch.diff',
+    'bugzilla_installation'             => 'bugzilla',
+    'bugzilla_path'                     => '/home/bugzilla/devel/htdocs/bugzilla',
+    'admin_user_login'                  => 'admin@my.company',
+    'admin_user_passwd'                 => '******',
+    'admin_user_username'               => 'admin',
+    'admin_user_api_key'                => 'zQ5TSBzq7tTZMtKYq9K1ZqJMjifKx3cPL7pIGk9Q',
+    'permanent_user'                    => 'permanent_user@my.company',
+    'unprivileged_user_login'           => 'no-privs@my.company',
+    'unprivileged_user_passwd'          => '******',
+    'unprivileged_user_username'        => 'no-privs',
+    'unprivileged_user_login_truncated' => 'no-privs@my',
+    'unprivileged_user_api_key'         => 'zQ5TSBzqP4nrdBKYq9Re4qJrjifKx3cK07pIGk9Q',
+    'QA_Selenium_TEST_user_login'       => 'QA-Selenium-TEST@my.company',
+    'QA_Selenium_TEST_user_passwd'      => '******',
+    'editbugs_user_login'               => 'editbugs@my.company',
+    'editbugs_user_passwd'              => '******',
+    'editbugs_user_api_key'             => 'zQ5ewBzq3gTrdBKYq9K1ZqJMjifKx3cKleE6k9TQ',
+    'canconfirm_user_login'             => 'canconfirm@my.company',
+    'canconfirm_user_passwd'            => '******',
+    'tweakparams_user_login'            => 'tweakparams@my.company',
+    'tweakparams_user_login_truncated'  => 'tweakparams@my',
+    'tweakparams_user_passwd'           => '******',
+    'disabled_user_login'               => 'disabled@my.company',
+    'disabled_user_passwd'              => '******',
+    'common_email'                      => '@my.company',
+    'test_extensions'                   => 0,
+};
diff --git a/xt/extensions/QA/Config.pm b/xt/extensions/QA/Config.pm
new file mode 100644 (file)
index 0000000..b4f6bc9
--- /dev/null
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::QA;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use constant NAME => 'QA';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/xt/extensions/QA/Extension.pm b/xt/extensions/QA/Extension.pm
new file mode 100644 (file)
index 0000000..5befe3e
--- /dev/null
@@ -0,0 +1,74 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::QA;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::QA::Util;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Util;
+use Bugzilla::Bug;
+use Bugzilla::User;
+
+our $VERSION = '1.0';
+
+sub page_before_template {
+    my ($self, $args) = @_;
+    return if $args->{page_id} ne 'qa/email_in.html';
+
+    my $template = Bugzilla->template;
+    my $cgi = Bugzilla->cgi;
+    print $cgi->header;
+
+    # Needed to make sure he can access and edit bugs.
+    my $user = Bugzilla::User->check($cgi->param('sender'));
+    Bugzilla->set_user($user);
+
+    my ($output, $tmpl_file);
+    my $action = $cgi->param('action') || '';
+    my $vars = { sender => $user, action => $action, pid => $$ };
+
+    if ($action eq 'create') {
+        $tmpl_file = 'qa/create_bug.txt.tmpl';
+    }
+    elsif ($action eq 'create_with_headers') {
+        $tmpl_file = 'qa/create_bug_with_headers.txt.tmpl';
+    }
+    elsif ($action =~ /^update(_with_headers)?$/) {
+        my $f = $1 || '';
+        $tmpl_file = "qa/update_bug$f.txt.tmpl";
+        my $bug = Bugzilla::Bug->check($cgi->param('bug_id'));
+        $vars->{bug_id} = $bug->id;
+    }
+    else {
+        ThrowUserError('unknown_action', { action => $action });
+    }
+
+    $template->process($tmpl_file, $vars, \$output)
+      or ThrowTemplateError($template->error());
+
+    my $file = "/tmp/email_in_$$.txt";
+    open(FH, '>', $file);
+    print FH $output;
+    close FH;
+
+    $output = `email_in.pl -v < $file 2>&1`;
+    unlink $file;
+
+    parse_output($output, $vars);
+
+    $template->process('qa/results.html.tmpl', $vars)
+      or ThrowTemplateError($template->error());
+}
+
+__PACKAGE__->NAME;
diff --git a/xt/extensions/QA/lib/Util.pm b/xt/extensions/QA/lib/Util.pm
new file mode 100644 (file)
index 0000000..e299adc
--- /dev/null
@@ -0,0 +1,28 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::QA::Util;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+    parse_output
+);
+
+sub parse_output {
+    my ($output, $vars) = @_;
+
+    $vars->{error} = ($output =~ /software error/i) ? 1 : 0;
+    $vars->{output} = $output;
+    $vars->{bug_id} ||= ($output =~ /Created bug (\d+)/i) ? $1 : undef;
+}
+
+1;
diff --git a/xt/extensions/QA/template/en/default/hook/README b/xt/extensions/QA/template/en/default/hook/README
new file mode 100644 (file)
index 0000000..3f1e487
--- /dev/null
@@ -0,0 +1,5 @@
+Template hooks go in this directory. Template hooks are called in normal
+Bugzilla templates like [% Hook.process('some-hook') %].
+More information about them can be found in the documentation of
+Bugzilla::Extension. (Do "perldoc Bugzilla::Extension" from the main
+Bugzilla directory to see that documentation.)
diff --git a/xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl b/xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl
new file mode 100644 (file)
index 0000000..bcb7510
--- /dev/null
@@ -0,0 +1,7 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
diff --git a/xt/extensions/QA/template/en/default/qa/README b/xt/extensions/QA/template/en/default/qa/README
new file mode 100644 (file)
index 0000000..604d00c
--- /dev/null
@@ -0,0 +1,16 @@
+Normal templates go in this directory. You can load them in your
+code like this:
+
+use Bugzilla::Error;
+my $template = Bugzilla->template;
+$template->process('qa/some-template.html.tmpl')
+  or ThrowTemplateError($template->error());
+
+That would be how to load a file called some-template.html.tmpl that
+was in this directory.
+
+Note that you have to be careful that the full path of your template
+never conflicts with a template that exists in Bugzilla or in
+another extension, or your template might override that template. That's why
+we created this directory called 'qa' for you, so you
+can put your templates in here to help avoid conflicts.
diff --git a/xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl b/xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl
new file mode 100644 (file)
index 0000000..5a83a6c
--- /dev/null
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+From: [% sender.email %]
+Subject: [% terms.Bug %] created using email_in.pl
+Content-Type: text/plain; charset="UTF-8"
+
+@product = TestProduct
+@component = TestComponent
+@version = unspecified
+
+This [% terms.bug %] has been created using email_in.pl (PID: [% pid %]).
diff --git a/xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl b/xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl
new file mode 100644 (file)
index 0000000..9973783
--- /dev/null
@@ -0,0 +1,33 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+From - Sat Jan  1 18:38:17 2011
+X-Account-Key: account2
+X-UIDL: GmailId12d42784d83cb4a4
+X-Mozilla-Status: 0011
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+Return-Path: <foo@bar.com>
+Received: from [192.168.0.2] (provider.com [51.162.153.14])
+        by mx.google.com with ESMTPS id m10sm12712256wbc.4.2011.01.01.09.38.01
+        (version=TLSv1/SSLv3 cipher=RC4-MD5);
+        Sat, 01 Jan 2011 09:38:01 -0800 (PST)
+Message-ID: <4D1F6580.9060076@gmail.com>
+Date: Sat, 01 Jan 2011 18:38:08 +0100
+User-Agent: Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.2.13) Gecko/20101207 Lightning/1.0b2 Thunderbird/3.1.7
+MIME-Version: 1.0
+From: [% sender.email %]
+Subject: [% terms.Bug %] created using email_in.pl (with email headers)
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 8bit
+
+@product = TestProduct
+@component = TestComponent
+@version = unspecified
+
+This [% terms.bug %] has been created using email_in.pl (PID: [% pid %]) with email headers.
diff --git a/xt/extensions/QA/template/en/default/qa/results.html.tmpl b/xt/extensions/QA/template/en/default/qa/results.html.tmpl
new file mode 100644 (file)
index 0000000..a2f8126
--- /dev/null
@@ -0,0 +1,28 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+[% title = BLOCK %]
+  [% IF error %]
+    Unexpected error
+  [% ELSE %]
+    email_in.pl output
+  [% END %]
+[% END %]
+
+[% PROCESS global/header.html.tmpl %]
+
+<h1>Action '[% action FILTER html %]' successful</h1>
+
+<div>
+<p>PID: <span id="pid">[% pid FILTER html %]</span></p>
+<p>[%+ terms.Bug %] ID: <span id="bug_id">[% bug_id FILTER html %]</span></p>
+
+<p>Full output:</p>
+<pre id="output">[% output FILTER html_light %]</pre>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl b/xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl
new file mode 100644 (file)
index 0000000..f37c002
--- /dev/null
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+From: [% sender.email %]
+Subject: [[% terms.Bug %] [%+ bug_id %]] This subject is ignored, only the [% terms.bug %] ID matters
+Content-Type: text/plain; charset="UTF-8"
+
+Comment added by email_in.pl (PID: [% pid %]). No other changes.
diff --git a/xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl b/xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl
new file mode 100644 (file)
index 0000000..fd093d3
--- /dev/null
@@ -0,0 +1,29 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+From - Sat Jan  1 18:38:17 2011
+X-Account-Key: account2
+X-UIDL: GmailId12d42784d83cb4a4
+X-Mozilla-Status: 0011
+X-Mozilla-Status2: 00000000
+X-Mozilla-Keys:
+Return-Path: <foo@bar.com>
+Received: from [192.168.0.2] (provider.com [51.162.153.14])
+        by mx.google.com with ESMTPS id m10sm12712256wbc.4.2011.01.01.09.38.01
+        (version=TLSv1/SSLv3 cipher=RC4-MD5);
+        Sat, 01 Jan 2011 09:38:01 -0800 (PST)
+Message-ID: <4D1F6580.9060076@gmail.com>
+Date: Sat, 01 Jan 2011 18:38:08 +0100
+User-Agent: Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.2.13) Gecko/20101207 Lightning/1.0b2 Thunderbird/3.1.7
+MIME-Version: 1.0
+From: [% sender.email %]
+Subject: [[% terms.Bug %] [%+ bug_id %]] This subject is ignored, only the [% terms.bug %] ID matters
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 8bit
+
+Comment added by email_in.pl (PID: [% pid %]) with email headers. No other changes.
diff --git a/xt/extensions/QA/web/README b/xt/extensions/QA/web/README
new file mode 100644 (file)
index 0000000..2345641
--- /dev/null
@@ -0,0 +1,7 @@
+Web-accessible files, like JavaScript, CSS, and images go in this
+directory. You can reference them directly in your HTML. For example,
+if you have a file called "style.css" and your extension is called
+"Foo", you would put it in "extensions/Foo/web/style.css", and then
+you could link to it in HTML like:
+
+<link href="extensions/Foo/web/style.css" rel="stylesheet" type="text/css">
\ No newline at end of file
diff --git a/xt/lib/Bugzilla/Test/Search.pm b/xt/lib/Bugzilla/Test/Search.pm
deleted file mode 100644 (file)
index ca3bba5..0000000
+++ /dev/null
@@ -1,987 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This module tests Bugzilla/Search.pm. It uses various constants
-# that are in Bugzilla::Test::Search::Constants, in xt/lib/.
-#
-# It does this by:
-# 1) Creating a bunch of field values. Each field value is
-#    randomly named and fully unique.
-# 2) Creating a bunch of bugs that use those unique field
-#    values. Each bug has different characteristics--see
-#    the comment above the NUM_BUGS constant for a description
-#    of each bug.
-# 3) Running searches using the combination of every search operator against
-#    every field. The tests that we run are described by the TESTS constant.
-#    Some of the operator/field combinations are known to be broken--
-#    these are listed in the KNOWN_BROKEN constant.
-# 4) For each search, we make sure that certain bugs are contained in
-#    the search, and certain other bugs are not contained in the search.
-#    The code for the operator/field tests is mostly in
-#    Bugzilla::Test::Search::FieldTest.
-# 5) After testing each operator/field combination's functionality, we
-#    do additional tests to make sure that there are no SQL injections
-#    possible via any operator/field combination. The code for the
-#    SQL Injection tests is in Bugzilla::Test::Search::InjectionTest.
-#
-# Generally, the only way that you should modify the behavior of this
-# script is by modifying the constants.
-
-package Bugzilla::Test::Search;
-
-use strict;
-use warnings;
-use Bugzilla::Attachment;
-use Bugzilla::Bug ();
-use Bugzilla::Constants;
-use Bugzilla::Field;
-use Bugzilla::Field::Choice;
-use Bugzilla::FlagType;
-use Bugzilla::Group;
-use Bugzilla::Install ();
-use Bugzilla::Test::Search::Constants;
-use Bugzilla::Test::Search::CustomTest;
-use Bugzilla::Test::Search::FieldTestNormal;
-use Bugzilla::Test::Search::OperatorTest;
-use Bugzilla::User ();
-use Bugzilla::Util qw(generate_random_password);
-
-use Carp;
-use DateTime;
-use Scalar::Util qw(blessed);
-
-###############
-# Constructor #
-###############
-
-sub new {
-    my ($class, $options) = @_;
-    return bless { options => $options }, $class;
-}
-
-#############
-# Accessors #
-#############
-
-sub options { return $_[0]->{options} }
-sub option { return $_[0]->{options}->{$_[1]} }
-
-sub num_tests {
-    my ($self) = @_;
-    my @top_operators = $self->top_level_operators;
-    my @all_operators = $self->all_operators;
-    my $top_operator_tests = $self->_total_operator_tests(\@top_operators);
-    my $all_operator_tests = $self->_total_operator_tests(\@all_operators);
-
-    my @fields = $self->all_fields;
-
-    # Basically, we run TESTS_PER_RUN tests for each field/operator combination.
-    my $top_combinations = $top_operator_tests * scalar(@fields);
-    my $all_combinations = $all_operator_tests * scalar(@fields);
-    # But we also have ORs, for which we run combinations^2 tests.
-    my $join_tests = $self->option('long')
-                     ? ($top_combinations * $all_combinations) : 0;
-    # And AND tests, which means we run 2x $join_tests;
-    $join_tests = $join_tests * 2;
-    # Also, because of NOT tests and Normal tests, we run 3x $top_combinations.
-    my $basic_tests = $top_combinations * 3;
-    my $operator_field_tests = ($basic_tests + $join_tests) * TESTS_PER_RUN;
-
-    # Then we test each field/operator combination for SQL injection.
-    my @injection_values = INJECTION_TESTS;
-    my $sql_injection_tests = scalar(@fields) * scalar(@top_operators)
-                              * scalar(@injection_values) * NUM_SEARCH_TESTS;
-
-    # This @{ [] } thing is the only reasonable way to get a count out of a
-    # constant array.
-    my $special_tests = scalar(@{ [SPECIAL_PARAM_TESTS, CUSTOM_SEARCH_TESTS] }) 
-                        * TESTS_PER_RUN;
-    
-    return $operator_field_tests + $sql_injection_tests + $special_tests;
-}
-
-sub _total_operator_tests {
-    my ($self, $operators) = @_;
-    
-    # Some operators have more than one test. Find those ones and add
-    # them to the total operator tests
-    my $extra_operator_tests;
-    foreach my $operator (@$operators) {
-        my $tests = TESTS->{$operator};
-        next if !$tests;
-        my $extra_num = scalar(@$tests) - 1;
-        $extra_operator_tests += $extra_num;
-    }
-    return scalar(@$operators) + $extra_operator_tests;
-    
-}
-
-sub all_operators {
-    my ($self) = @_;
-    if (not $self->{all_operators}) {
-        
-        my @operators;
-        if (my $limit_operators = $self->option('operators')) {
-            @operators = split(',', $limit_operators);
-        }
-        else {
-            @operators = sort (keys %{ Bugzilla::Search::OPERATORS() });
-        }
-        # "substr" is just a backwards-compatibility operator, same as "substring".
-        @operators = grep { $_ ne 'substr' } @operators;
-        $self->{all_operators} = \@operators;
-    }
-    return @{ $self->{all_operators} };
-}
-
-sub all_fields {
-    my $self = shift;
-    if (not $self->{all_fields}) {
-        $self->_create_custom_fields();
-        my @fields = @{ Bugzilla->fields };
-        @fields = sort { $a->name cmp $b->name } @fields;
-        $self->{all_fields} = \@fields;
-    }
-    return @{ $self->{all_fields} };
-}
-
-sub top_level_operators {
-    my ($self) = @_;
-    if (!$self->{top_level_operators}) {
-        my @operators;
-        my $limit_top = $self->option('top-operators');
-        if ($limit_top) {
-            @operators = split(',', $limit_top);
-        }
-        else {
-            @operators = $self->all_operators;
-        }
-        $self->{top_level_operators} = \@operators;
-    }
-    return @{ $self->{top_level_operators} };
-}
-
-sub text_fields {
-    my ($self) = @_;
-    my @text_fields = grep { $_->type == FIELD_TYPE_TEXTAREA
-                             or $_->type == FIELD_TYPE_FREETEXT } $self->all_fields;
-    @text_fields = map { $_->name } @text_fields;
-    push(@text_fields, qw(short_desc status_whiteboard bug_file_loc see_also));
-    return @text_fields;
-}
-
-sub bugs {
-    my $self = shift;
-    $self->{bugs} ||= [map { $self->_create_one_bug($_) } (1..NUM_BUGS)];
-    return @{ $self->{bugs} };
-}
-
-# Get a numbered bug.
-sub bug {
-    my ($self, $number) = @_;
-    return ($self->bugs)[$number - 1];
-}
-
-sub admin {
-    my $self = shift;
-    if (!$self->{admin_user}) {
-        my $admin = create_user("admin");
-        Bugzilla::Install::make_admin($admin);
-        $self->{admin_user} = $admin;
-    }
-    # We send back a fresh object every time, to make sure that group
-    # memberships are always up-to-date.
-    return new Bugzilla::User($self->{admin_user}->id);
-}
-
-sub nobody {
-    my $self = shift;
-    $self->{nobody} ||= Bugzilla::Group->create({ name => "nobody-" . random(),
-        description => "Nobody", isbuggroup => 1 });
-    return $self->{nobody};
-}
-sub everybody {
-    my ($self) = @_;
-    $self->{everybody} ||= create_group('To The Limit');
-    return $self->{everybody};
-}
-
-sub bug_create_value {
-    my ($self, $number, $field) = @_;
-    $field = $field->name if blessed($field);
-    if ($number == 6 and $field ne 'alias') {
-        $number = 1;
-    }
-    my $extra_values = $self->_extra_bug_create_values->{$number};
-    if (exists $extra_values->{$field}) {
-        return $extra_values->{$field};
-    }
-    return $self->_bug_create_values->{$number}->{$field};
-}
-sub bug_update_value {
-    my ($self, $number, $field) = @_;
-    $field = $field->name if blessed($field);
-    if ($number == 6 and $field ne 'alias') {
-        $number = 1;
-    }
-    return $self->_bug_update_values->{$number}->{$field};
-}
-
-# Values used to create the bugs.
-sub _bug_create_values {
-    my $self = shift;
-    return $self->{bug_create_values} if $self->{bug_create_values};
-    my %values;
-    foreach my $number (1..NUM_BUGS) {
-        $values{$number} = $self->_create_field_values($number, 'for create');
-    }
-    $self->{bug_create_values} = \%values;
-    return $self->{bug_create_values};
-}
-# Values as they existed on the bug, at creation time. Used by the
-# changedfrom tests.
-sub _extra_bug_create_values {
-    my $self = shift;
-    $self->{extra_bug_create_values} ||= { map { $_ => {} } (1..NUM_BUGS) };
-    return $self->{extra_bug_create_values};
-}
-
-# Values used to update the bugs after they are created.
-sub _bug_update_values {
-    my $self = shift;
-    return $self->{bug_update_values} if $self->{bug_update_values};
-    my %values;
-    foreach my $number (1..NUM_BUGS) {
-        $values{$number} = $self->_create_field_values($number);
-    }
-    $self->{bug_update_values} = \%values;
-    return $self->{bug_update_values};
-}
-
-##############################
-# General Helper Subroutines #
-##############################
-
-sub random {
-    $_[0] ||= FIELD_SIZE;
-    generate_random_password(@_);
-}
-
-# We need to use a custom timestamp for each create() and update(),
-# because the database returns the same value for LOCALTIMESTAMP(0)
-# for the entire transaction, and we need each created bug to have
-# its own creation_ts and delta_ts.
-sub timestamp {
-    my ($day, $second) = @_;
-    return DateTime->new(
-        year   => 2037,
-        month  => 1,
-        day    => $day,
-        hour   => 12,
-        minute => $second,
-        second => 0,
-        # We make it floating because the timezone doesn't matter for our uses,
-        # and we want totally consistent behavior across all possible machines.
-        time_zone => 'floating',
-    );
-}
-
-sub create_keyword {
-    my ($number) = @_;
-    return Bugzilla::Keyword->create({
-        name => "$number-keyword-" . random(),
-        description => "Keyword $number" });
-}
-
-sub create_user {
-    my ($prefix) = @_;
-    my $user_name = $prefix . '-' . random(15) . "@" . random(12)
-                    . "." . random(3);
-    my $user_realname = $prefix . '-' . random();
-    my $user = Bugzilla::User->create({
-        login_name => $user_name,
-        realname   => $user_realname,
-        cryptpassword => '*',
-    });
-    return $user;
-}
-
-sub create_group {
-    my ($prefix) = @_;
-    return Bugzilla::Group->create({
-        name => "$prefix-group-" . random(), description => "Everybody $prefix",
-        userregexp => '.*', isbuggroup => 1 });
-}
-
-sub create_legal_value {
-    my ($field, $number) = @_;
-    my $type = Bugzilla::Field::Choice->type($field);
-    my $field_name = $field->name;
-    return $type->create({ value => "$number-$field_name-" . random(),
-                           is_open => 0 });
-}
-
-#########################
-# Custom Field Creation #
-#########################
-
-sub _create_custom_fields {
-    my ($self) = @_;
-    return if !$self->option('add-custom-fields');
-    
-    while (my ($type, $name) = each %{ CUSTOM_FIELDS() }) {
-        my $exists = new Bugzilla::Field({ name => $name });
-        next if $exists;
-        Bugzilla::Field->create({
-            name => $name,
-            type => $type,
-            description => "Search Test Field $name",
-            enter_bug => 1,
-            custom => 1,
-            buglist => 1,
-            is_mandatory => 0,
-        });
-    }
-}
-
-########################
-# Field Value Creation #
-########################
-
-sub _create_field_values {
-    my ($self, $number, $for_create) = @_;
-    my $dbh = Bugzilla->dbh;
-    
-    Bugzilla->set_user($self->admin);
-
-    my @selects = grep { $_->is_select } $self->all_fields;
-    my %values;
-    foreach my $field (@selects) {
-        next if $field->is_abnormal;
-        $values{$field->name} = create_legal_value($field, $number)->name;
-    }
-
-    my $group = create_group($number);
-    $values{groups} = [$group->name];
-
-    $values{'keywords'} = create_keyword($number)->name;
-
-    foreach my $field (qw(assigned_to qa_contact reporter cc)) {
-        $values{$field} = create_user("$number-$field")->login;
-    }
-
-    my $classification = Bugzilla::Classification->create(
-        { name => "$number-classification-" . random() });
-    $classification = $classification->name;
-
-    my $version = "$number-version-" . random();
-    my $milestone = "$number-tm-" . random(15);
-    my $product = Bugzilla::Product->create({
-        name => "$number-product-" . random(),
-        description => 'Created by t/search.t',
-        defaultmilestone => $milestone,
-        classification => $classification,
-        version => $version,
-        allows_unconfirmed => 1,
-    });
-    foreach my $item ($group, $self->nobody) {
-        $product->set_group_controls($item,
-            { membercontrol => CONTROLMAPSHOWN,
-              othercontrol => CONTROLMAPNA });
-    }
-    # $product->update() is called lower down.
-    my $component = Bugzilla::Component->create({
-        product => $product, name => "$number-component-" . random(),
-        initialowner => create_user("$number-defaultowner")->login,
-        initialqacontact => create_user("$number-defaultqa")->login,
-        initial_cc => [create_user("$number-initcc")->login],
-        description => "Component $number" });
-    
-    $values{'product'} = $product->name;
-    $values{'component'} = $component->name;
-    $values{'target_milestone'} = $milestone;
-    $values{'version'} = $version;
-
-    foreach my $field ($self->text_fields) {
-        # We don't add a - after $field for the text fields, because
-        # if we do, fulltext searching for short_desc pulls out
-        # "short_desc" as a word and matches it in every bug.
-        my $value = "$number-$field" . random();
-        if ($field eq 'bug_file_loc' or $field eq 'see_also') {
-            $value = "http://$value-" . random(3)
-                     . "/show_bug.cgi?id=$number";
-        }
-        $values{$field} = $value;
-    }
-    $values{'tag'} = ["$number-tag-" . random()];
-    
-    my @date_fields = grep { $_->type == FIELD_TYPE_DATETIME } $self->all_fields;
-    foreach my $field (@date_fields) {
-        # We use 03 as the month because that differs from our creation_ts,
-        # delta_ts, and deadline. (It's nice to have recognizable values
-        # for each field when debugging.)
-        my $second = $for_create ? $number : $number + 1;
-        $values{$field->name} = "2037-03-0$number 12:34:0$second";
-    }
-
-    $values{alias} = "$number-alias-" . random(12);
-
-    # Prefixing the original comment with "description" makes the
-    # lesserthan and greaterthan tests behave predictably.
-    my $comm_prefix = $for_create ? "description-" : '';
-    $values{comment} = "$comm_prefix$number-comment-" . random()
-                               . ' ' . random();
-
-    my @flags;
-    my $setter = create_user("$number-setters.login_name");
-    my $requestee = create_user("$number-requestees.login_name");
-    $values{set_flags} = _create_flags($number, $setter, $requestee);
-
-    my $month = $for_create ? "12" : "02";
-    $values{'deadline'} = "2037-$month-0$number";
-    my $estimate_times = $for_create ? 10 : 1;
-    $values{estimated_time} = $estimate_times * $number;
-
-    $values{attachment} = _get_attach_values($number, $for_create);
-
-    # Some things only happen on the first bug.
-    if ($number == 1) {
-        # We use 6 as the prefix for the extra values, because bug 6's values
-        # don't otherwise get used (since bug 6 is created as a clone of
-        # bug 1). This also makes sure that our greaterthan/lessthan
-        # tests work properly.
-        my $extra_group = create_group(6);
-        $product->set_group_controls($extra_group,
-            { membercontrol => CONTROLMAPSHOWN,
-              othercontrol => CONTROLMAPNA });
-        $values{groups} = [$values{groups}->[0], $extra_group->name];
-        my $extra_keyword = create_keyword(6);
-        $values{keywords} = [$values{keywords}, $extra_keyword->name];
-        my $extra_cc = create_user("6-cc");
-        $values{cc} = [$values{cc}, $extra_cc->login];
-        my @multi_selects = grep { $_->type == FIELD_TYPE_MULTI_SELECT }
-                                 $self->all_fields;
-        foreach my $field (@multi_selects) {
-            my $new_value = create_legal_value($field, 6);
-            my $name = $field->name;
-            $values{$name} = [$values{$name}, $new_value->name];
-        }
-        push(@{ $values{'tag'} }, "6-tag-" . random());
-    }
-
-    # On bug 5, any field that *can* be left empty, *is* left empty.
-    if ($number == 5) {
-        my @set_fields = grep { $_->type == FIELD_TYPE_SINGLE_SELECT }
-                         $self->all_fields;
-        @set_fields = map { $_->name } @set_fields;
-        push(@set_fields, qw(short_desc version reporter));
-        foreach my $key (keys %values) {
-            delete $values{$key} unless grep { $_ eq $key } @set_fields;
-        }
-    }
-
-    $product->update();
-
-    return \%values;
-}
-
-# Flags
-sub _create_flags {
-    my ($number, $setter, $requestee) = @_;
-
-    my $flagtypes = _create_flagtypes($number);
-
-    my %flags;
-    foreach my $type (qw(a b)) {
-        $flags{$type} = _get_flag_values(@_, $flagtypes->{$type});
-    }
-    return \%flags;
-}
-
-sub _create_flagtypes {
-    my ($number) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $name = "$number-flag-" . random();
-    my $desc = "FlagType $number";
-
-    my %flagtypes; 
-    foreach my $target (qw(a b)) {
-         $dbh->do("INSERT INTO flagtypes
-                  (name, description, target_type, is_requestable, 
-                   is_requesteeble, is_multiplicable, cc_list)
-                   VALUES (?,?,?,1,1,1,'')",
-                   undef, $name, $desc, $target);
-         my $id = $dbh->bz_last_key('flagtypes', 'id');
-         $dbh->do('INSERT INTO flaginclusions (type_id) VALUES (?)',
-                  undef, $id);
-         my $flagtype = new Bugzilla::FlagType($id);
-         $flagtypes{$target} = $flagtype;
-    }
-    return \%flagtypes;
-}
-
-sub _get_flag_values {
-    my ($number, $setter, $requestee, $flagtype) = @_;
-
-    my @set_flags;
-    if ($number <= 2) {
-        foreach my $value (qw(? - + ?)) {
-            my $flag = { type_id => $flagtype->id, status => $value,
-                         setter => $setter, flagtype => $flagtype };
-            push(@set_flags, $flag);
-        }
-        $set_flags[0]->{requestee} = $requestee->login;
-    }
-    else {
-        @set_flags = ({ type_id => $flagtype->id, status => '+',
-                        setter => $setter, flagtype => $flagtype });
-    }
-    return \@set_flags;
-}
-
-# Attachments
-sub _get_attach_values {
-    my ($number, $for_create) = @_;
-
-    my $boolean = $number == 1 ? 1 : 0;
-    if ($for_create) {
-        $boolean = !$boolean ? 1 : 0;
-    }
-    my $ispatch = $for_create ? 'ispatch' : 'is_patch';
-    my $isobsolete = $for_create ? 'isobsolete' : 'is_obsolete';
-    my $isprivate = $for_create ? 'isprivate' : 'is_private';
-    my $mimetype = $for_create ? 'mimetype' : 'content_type';
-
-    my %values = (
-        description => "$number-attach_desc-" . random(),
-        filename => "$number-filename-" . random(),
-        $ispatch => $boolean,
-        $isobsolete => $boolean,
-        $isprivate => $boolean,
-        $mimetype => "text/x-$number-" . random(),
-    );
-    if ($for_create) {
-        $values{data} = "$number-data-" . random() . random();
-    }
-    return \%values;
-}
-
-################
-# Bug Creation #
-################
-
-sub _create_one_bug {
-    my ($self, $number) = @_;
-    my $dbh = Bugzilla->dbh;
-    
-    # We need bug 6 to have a unique alias that is not a clone of bug 1's,
-    # so we get the alias separately from the other parameters.
-    my $alias = $self->bug_create_value($number, 'alias');
-    my $update_alias = $self->bug_update_value($number, 'alias');
-    
-    # Otherwise, make bug 6 a clone of bug 1.
-    my $real_number = $number;
-    $number = 1 if $number == 6;
-    
-    my $reporter = $self->bug_create_value($number, 'reporter');
-    Bugzilla->set_user(Bugzilla::User->check($reporter));
-    
-    # We create the bug with one set of values, and then we change it
-    # to have different values.
-    my %params = %{ $self->_bug_create_values->{$number} };
-    $params{alias} = $alias;
-    
-    # There are some things in bug_create_values that shouldn't go into
-    # create().
-    delete @params{qw(attachment set_flags tag)};
-    
-    my ($status, $resolution, $see_also) = 
-        delete @params{qw(bug_status resolution see_also)};
-    # All the bugs are created with everconfirmed = 0.
-    $params{bug_status} = 'UNCONFIRMED';
-    my $bug = Bugzilla::Bug->create(\%params);
-    
-    # These are necessary for the changedfrom tests.
-    my $extra_values = $self->_extra_bug_create_values->{$number};
-    foreach my $field (qw(comments remaining_time percentage_complete
-                         keyword_objects everconfirmed dependson blocked
-                         groups_in classification actual_time))
-    {
-        $extra_values->{$field} = $bug->$field;
-    }
-    $extra_values->{reporter_accessible} = $number == 1 ? 0 : 1;
-    $extra_values->{cclist_accessible}   = $number == 1 ? 0 : 1;
-    
-    if ($number == 5) {
-        # Bypass Bugzilla::Bug--we don't want any changes in bugs_activity
-        # for bug 5.
-        $dbh->do('UPDATE bugs SET qa_contact = NULL, reporter_accessible = 0,
-                                  cclist_accessible = 0 WHERE bug_id = ?',
-                 undef, $bug->id);
-        $dbh->do('DELETE FROM cc WHERE bug_id = ?', undef, $bug->id);
-        my $ts = '1970-01-01 00:00:00';
-        $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ?
-                   WHERE bug_id = ?', undef, $ts, $ts, $bug->id);
-        $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?',
-                 undef, $ts, $bug->id);
-        $bug->{creation_ts} = $ts;
-        $extra_values->{see_also} = [];
-    }
-    else {
-        # Manually set the creation_ts so that each bug has a different one.
-        #
-        # Also, manually update the resolution and bug_status, because
-        # we want to see both of them change in bugs_activity, so we
-        # have to start with values for both (and as of the time when I'm
-        # writing this test, Bug->create doesn't support setting resolution).
-        #
-        # Same for see_also.
-        my $timestamp = timestamp($number, $number - 1);
-        my $creation_ts = $timestamp->ymd . ' ' . $timestamp->hms;
-        $bug->{creation_ts} = $creation_ts;
-        $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?',
-                 undef, $creation_ts, $bug->id);
-        $dbh->do('UPDATE bugs SET creation_ts = ?, bug_status = ?,
-                  resolution = ? WHERE bug_id = ?',
-                 undef, $creation_ts, $status, $resolution, $bug->id);
-        $dbh->do('INSERT INTO bug_see_also (bug_id, value, class) VALUES (?,?,?)',
-                 undef, $bug->id, $see_also, 'Bugzilla::BugUrl::Bugzilla');
-        $extra_values->{see_also} = $bug->see_also;
-
-        # All the tags must be created as the admin user, so that the
-        # admin user can find them, later.
-        my $original_user = Bugzilla->user;
-        Bugzilla->set_user($self->admin);
-        my $tags = $self->bug_create_value($number, 'tag');
-        $bug->add_tag($_) foreach @$tags;
-        $extra_values->{tags} = $tags;
-        Bugzilla->set_user($original_user);
-
-        if ($number == 1) {
-            # Bug 1 needs to start off with reporter_accessible and
-            # cclist_accessible being 0, so that when we change them to 1,
-            # that change shows up in bugs_activity.
-            $dbh->do('UPDATE bugs SET reporter_accessible = 0,
-                      cclist_accessible = 0 WHERE bug_id = ?',
-                      undef, $bug->id);
-            # Bug 1 gets three comments, so that longdescs.count matches it
-            # uniquely. The third comment is added in the middle, so that the
-            # last comment contains all of the important data, like work_time.
-            $bug->add_comment("1-comment-" . random(100));
-        }
-        
-        my %update_params = %{ $self->_bug_update_values->{$number} };
-        my %reverse_map = reverse %{ Bugzilla::Bug->FIELD_MAP };
-        foreach my $db_name (keys %reverse_map) {
-            next if $db_name eq 'comment';
-            next if $db_name eq 'status_whiteboard';
-            if (exists $update_params{$db_name}) {
-                my $update_name = $reverse_map{$db_name};
-                $update_params{$update_name} = delete $update_params{$db_name};
-            }
-        }
-        
-        my ($new_status, $new_res) = 
-            delete @update_params{qw(status resolution)};
-        # Bypass the status workflow.
-        $bug->{bug_status} = $new_status;
-        $bug->{resolution} = $new_res;
-        $bug->{everconfirmed} = 1 if $number == 1;
-        
-        # add/remove/set fields.
-        $update_params{keywords} = { set => $update_params{keywords} };
-        $update_params{groups} = { add => $update_params{groups},
-                                   remove => $bug->groups_in };
-        my @cc_remove = map { $_->login } @{ $bug->cc_users };
-        my $cc_new = $update_params{cc};
-        my @cc_add = ref($cc_new) ? @$cc_new : ($cc_new);
-        # We make the admin an explicit CC on bug 1 (but not on bug 6), so
-        # that we can test the %user% pronoun properly.
-        if ($real_number == 1) {
-            push(@cc_add, $self->admin->login);
-        }
-        $update_params{cc} = { add => \@cc_add, remove => \@cc_remove };
-        my $see_also_remove = $bug->see_also;
-        my $see_also_add = [$update_params{see_also}];
-        $update_params{see_also} = { add => $see_also_add, 
-                                     remove => $see_also_remove };
-        $update_params{comment} = { body => $update_params{comment} };
-        $update_params{work_time} = $number;
-        # Setting work_time kills the remaining_time, so we need to
-        # preserve that. We add 8 because that produces an integer
-        # percentage_complete for bug 1, which is necessary for
-        # accurate "equals"-type searching.
-        $update_params{remaining_time} = $number + 8;
-        $update_params{reporter_accessible} = $number == 1 ? 1 : 0;
-        $update_params{cclist_accessible} = $number == 1 ? 1 : 0;
-        $update_params{alias} = $update_alias;
-
-        $bug->set_all(\%update_params);
-        my $flags = $self->bug_create_value($number, 'set_flags')->{b};
-        $bug->set_flags([], $flags);
-        $timestamp->set(second => $number);
-        $bug->update($timestamp->ymd . ' ' . $timestamp->hms);
-        $extra_values->{flags} = $bug->flags;
-        
-        # It's not generally safe to do update() multiple times on
-        # the same Bug object.
-        $bug = new Bugzilla::Bug($bug->id);
-        my $update_flags = $self->bug_update_value($number, 'set_flags')->{b};
-        $_->{status} = 'X' foreach @{ $bug->flags };
-        $bug->set_flags($bug->flags, $update_flags);
-        if ($number == 1) {
-            my $comment_id = $bug->comments->[-1]->id;
-            $bug->set_comment_is_private({ $comment_id => 1 });
-        }
-        $bug->update($bug->delta_ts);
-        
-        my $attach_create = $self->bug_create_value($number, 'attachment');
-        my $attachment = Bugzilla::Attachment->create({
-            bug => $bug,
-            creation_ts => $creation_ts,
-            %$attach_create });
-        # Store for the changedfrom tests.
-        $extra_values->{attachments} = 
-            [new Bugzilla::Attachment($attachment->id)];
-        
-        my $attach_update = $self->bug_update_value($number, 'attachment');
-        $attachment->set_all($attach_update);
-        # In order to keep the mimetype on the ispatch attachment,
-        # we need to bypass the validator.
-        $attachment->{mimetype} = $attach_update->{content_type};
-        my $attach_flags = $self->bug_update_value($number, 'set_flags')->{a};
-        $attachment->set_flags([], $attach_flags);
-        $attachment->update($bug->delta_ts);
-    }
-    
-    # Values for changedfrom.
-    $extra_values->{creation_ts} = $bug->creation_ts;
-    $extra_values->{delta_ts}    = $bug->creation_ts;
-    
-    return new Bugzilla::Bug($bug->id);
-}
-
-###################################
-# Test::Builder Memory Efficiency #
-###################################
-
-# Test::Builder stores information for each test run, but Test::Harness
-# and TAP::Harness don't actually need this information. When we run 60
-# million tests, the history eats up all our memory. (After about
-# 1 million tests, memory usage is around 1 GB.)
-#
-# The only part of the history that Test::More actually *uses* is the "ok"
-# field, which we store more efficiently, in an array, and then we re-populate
-# the Test_Results in Test::Builder at the end of the test.
-sub clean_test_history {
-    my ($self) = @_;
-    return if !$self->option('long');
-    my $builder = Test::More->builder;
-    my $current_test = $builder->current_test;
-
-    # I don't use details() because I don't want to copy the array.
-    my $results = $builder->{Test_Results};
-    my $check_test = $current_test - 1;
-    while (my $result = $results->[$check_test]) {
-        last if !$result;
-        $self->test_success($check_test, $result->{ok});
-        $check_test--;
-    }
-
-    # Truncate the test history array, but retain the current test number.
-    $builder->{Test_Results} = [];
-    $builder->{Curr_Test} = $current_test;
-}
-
-sub test_success {
-    my ($self, $index, $status) = @_;
-    $self->{test_success}->[$index] = $status;
-    return $self->{test_success};
-}
-
-sub repopulate_test_results {
-    my ($self) = @_;
-    return if !$self->option('long');
-    $self->clean_test_history();
-    # We create only two hashes, for memory efficiency.
-    my %ok = ( ok => 1 );
-    my %not_ok = ( ok => 0 );
-    my @results;
-    foreach my $success (@{ $self->{test_success} }) {
-        push(@results, $success ? \%ok : \%not_ok);
-    }
-    my $builder = Test::More->builder;
-    $builder->{Test_Results} = \@results;
-}
-
-##########
-# Caches #
-##########
-
-# When doing AND and OR tests, we essentially test the same field/operator
-# combinations over and over. So, if we're going to be running those tests,
-# we cache the translated_value of the FieldTests globally so that we don't
-# have to re-run the value-translation code every time (which can be pretty
-# slow).
-sub value_translation_cache {
-    my ($self, $field_test, $value) = @_;
-    return if !$self->option('long');
-    my $test_name = $field_test->name;
-    if (@_ == 3) {
-        $self->{value_translation_cache}->{$test_name} = $value;
-    }
-    return $self->{value_translation_cache}->{$test_name};
-}
-
-# When doing AND/OR tests, the value for transformed_value_was_equal
-# (see Bugzilla::Test::Search::FieldTest) won't be recalculated
-# if we pull our values from the value_translation_cache. So we need
-# to also cache the values for transformed_value_was_equal.
-sub was_equal_cache {
-    my ($self, $field_test, $number, $value) = @_;
-    return if !$self->option('long');
-    my $test_name = $field_test->name;
-    if (@_ == 4) {
-        $self->{tvwe_cache}->{$test_name}->{$number} = $value;
-    }
-    return $self->{tvwe_cache}->{$test_name}->{$number};
-}
-
-#############
-# Main Test #
-#############
-
-sub run {
-    my ($self) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    # We want backtraces on any "die" message or any warning.
-    # Otherwise it's hard to trace errors inside of Bugzilla::Search from
-    # reading automated test run results.
-    local $SIG{__WARN__} = \&Carp::cluck;
-    local $SIG{__DIE__}  = \&Carp::confess;
-
-    $dbh->bz_start_transaction();
-    
-    # Some parameters need to be set in order for the tests to function
-    # properly.
-    my $everybody = $self->everybody;
-    my $params = Bugzilla->params;
-    local $params->{'useclassification'} = 1;
-    local $params->{'useqacontact'} = 1;
-    local $params->{'usetargetmilestone'} = 1;
-    local $params->{'mail_delivery_method'} = 'None';
-    local $params->{'timetrackinggroup'} = $everybody->name;
-    local $params->{'insidergroup'} = $everybody->name;
-
-    $self->_setup_bugs();
-    
-    # Even though _setup_bugs set us as an admin, we want to be sure at
-    # this point that we have an admin with refreshed group memberships.
-    Bugzilla->set_user($self->admin);
-    foreach my $test (CUSTOM_SEARCH_TESTS) {
-        my $custom_test = new Bugzilla::Test::Search::CustomTest($test, $self);
-        $custom_test->run();
-    }
-    foreach my $test (SPECIAL_PARAM_TESTS) {
-        my $operator_test =
-            new Bugzilla::Test::Search::OperatorTest($test->{operator}, $self);
-        my $field = Bugzilla::Field->check($test->{field});
-        my $special_test = new Bugzilla::Test::Search::FieldTestNormal(
-            $operator_test, $field, $test);
-        $special_test->run();
-    }
-    foreach my $operator ($self->top_level_operators) {
-        my $operator_test =
-            new Bugzilla::Test::Search::OperatorTest($operator, $self);
-        $operator_test->run();
-    }
-
-    # Rollbacks won't get rid of bugs_fulltext entries, so we do that ourselves.
-    my @bug_ids = map { $_->id } $self->bugs;
-    my $bug_id_string = join(',', @bug_ids);
-    $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id IN ($bug_id_string)");
-    $dbh->bz_rollback_transaction();
-    $self->repopulate_test_results();
-}
-
-# This makes a few changes to the bugs after they're created--changes
-# that can only be done after all the bugs have been created.
-sub _setup_bugs {
-    my ($self) = @_;
-    $self->_setup_dependencies();
-    $self->_set_bug_id_fields();
-    $self->_protect_bug_6();
-}
-sub _setup_dependencies {
-    my ($self) = @_;
-    my $dbh = Bugzilla->dbh;
-    
-    # Set up depedency relationships between the bugs.
-    # Bug 1 + 6 depend on bug 2 and block bug 3.
-    my $bug2 = $self->bug(2);
-    my $bug3 = $self->bug(3);
-    foreach my $number (1,6) {
-        my $bug = $self->bug($number);
-        my @original_delta = ($bug2->delta_ts, $bug3->delta_ts);
-        Bugzilla->set_user($bug->reporter);
-        $bug->set_dependencies([$bug2->id], [$bug3->id]);
-        $bug->update($bug->delta_ts);
-        # Setting dependencies changed the delta_ts on bug2 and bug3, so
-        # re-set them back to what they were before. However, we leave
-        # the correct update times in bugs_activity, so that the changed*
-        # searches still work right.
-        my $set_delta = $dbh->prepare(
-            'UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
-        foreach my $row ([$original_delta[0], $bug2->id], 
-                         [$original_delta[1], $bug3->id])
-        {
-            $set_delta->execute(@$row);
-        }
-    }
-}
-
-sub _set_bug_id_fields {
-    my ($self) = @_;
-    # BUG_ID fields couldn't be set before, because before we create bug 1,
-    # we don't necessarily have any valid bug ids.)
-    my @bug_id_fields = grep { $_->type == FIELD_TYPE_BUG_ID }
-                             $self->all_fields;
-    foreach my $number (1..NUM_BUGS) {
-        my $bug = $self->bug($number);
-        $number = 1 if $number == 6;
-        next if $number == 5;
-        my $other_bug = $self->bug($number + 1);
-        Bugzilla->set_user($bug->reporter);
-        foreach my $field (@bug_id_fields) {
-            $bug->set_custom_field($field, $other_bug->id);
-            $bug->update($bug->delta_ts);
-        }
-    }
-}
-
-sub _protect_bug_6 {
-    my ($self) = @_;
-    my $dbh = Bugzilla->dbh;
-    
-    Bugzilla->set_user($self->admin);
-    
-    # Put bug6 in the nobody group.
-    my $nobody = $self->nobody;
-    # We pull it newly from the DB to be sure it's safe to call update()
-    # on.
-    my $bug6 = new Bugzilla::Bug($self->bug(6)->id);
-    $bug6->add_group($nobody);
-    $bug6->update($bug6->delta_ts);
-    
-    # Remove the admin (and everybody else) from the $nobody group.
-    $dbh->do('DELETE FROM group_group_map 
-               WHERE grantor_id = ? OR member_id = ?', undef,
-             $nobody->id, $nobody->id);
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/AndTest.pm b/xt/lib/Bugzilla/Test/Search/AndTest.pm
deleted file mode 100644 (file)
index f34ba1f..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This test combines two field/operator combinations using AND in
-# a single boolean chart.
-package Bugzilla::Test::Search::AndTest;
-use parent qw(Bugzilla::Test::Search::OrTest);
-
-use Bugzilla::Test::Search::Constants;
-use List::MoreUtils qw(all);
-
-use constant type => 'AND';
-
-#############
-# Accessors #
-#############
-
-# In an AND test, bugs ARE supposed to be contained only if they are contained
-# by ALL tests.
-sub bug_is_contained {
-    my ($self, $number) = @_;
-    return all { $_->bug_is_contained($number) } $self->field_tests;
-}
-
-sub _bug_will_actually_be_contained {
-    my ($self, $number) = @_;
-    return all { $_->will_actually_contain_bug($number) } $self->field_tests;
-}
-
-##############################
-# Bugzilla::Search arguments #
-##############################
-
-sub search_params {
-    my ($self) = @_;
-    my @all_params = map { $_->search_params } $self->field_tests;
-    my %params;
-    my $chart = 0;
-    foreach my $item (@all_params) {
-        $params{"field0-$chart-0"} = $item->{'field0-0-0'};
-        $params{"type0-$chart-0"}  = $item->{'type0-0-0'};
-        $params{"value0-$chart-0"} = $item->{'value0-0-0'};
-        $chart++;
-    }
-    return \%params;
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/Constants.pm b/xt/lib/Bugzilla/Test/Search/Constants.pm
deleted file mode 100644 (file)
index 5d84ec6..0000000
+++ /dev/null
@@ -1,1203 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-
-# These are constants used by Bugzilla::Test::Search.
-# See the comment at the top of that package for a general overview
-# of how the search test works, and how the constants are used.
-# More detailed information on each constant is available in the comments
-# in this file.
-package Bugzilla::Test::Search::Constants;
-use parent qw(Exporter);
-use Bugzilla::Constants;
-use Bugzilla::Util qw(generate_random_password);
-
-our @EXPORT = qw(
-    ATTACHMENT_FIELDS
-    BROKEN_NOT
-    COLUMN_TRANSLATION
-    COMMENT_FIELDS
-    CUSTOM_FIELDS
-    CUSTOM_SEARCH_TESTS
-    FIELD_SIZE
-    FIELD_SUBSTR_SIZE
-    FLAG_FIELDS
-    INJECTION_BROKEN_FIELD
-    INJECTION_BROKEN_OPERATOR
-    INJECTION_TESTS
-    KNOWN_BROKEN
-    NUM_BUGS
-    NUM_SEARCH_TESTS
-    SKIP_FIELDS
-    SPECIAL_PARAM_TESTS
-    SUBSTR_NO_FIELD_ADD
-    SUBSTR_SIZE
-    TESTS
-    TESTS_PER_RUN
-    USER_FIELDS
-);
-
-# Bug 1 is designed to be found by all the "equals" tests. It has
-# multiple values for several fields where other fields only have
-# one value.
-#
-# Bug 2 and 3 have a dependency relationship with Bug 1,
-# but show up in "not equals" tests. We do use bug 2 in multiple-value
-# tests.
-#
-# Bug 4 should never show up in any equals test, and has no relationship
-# with any other bug. However, it does have all its fields set.
-#
-# Bug 5 only has values set for mandatory fields, to expose problems
-# that happen with "not equals" tests failing to catch bugs that don't
-# have a value set at all.
-#
-# Bug 6 is a clone of Bug 1, but is in a group that the searcher isn't
-# in.
-use constant NUM_BUGS => 6;
-
-# How many tests there are for each operator/field combination other
-# than the "contains" tests.
-use constant NUM_SEARCH_TESTS => 3;
-# This is how many tests get run for each field/operator.
-use constant TESTS_PER_RUN => NUM_SEARCH_TESTS + NUM_BUGS;
-
-# This is how many random characters we generate for most fields' names.
-# (Some fields can't be this long, though, so they have custom lengths
-# in Bugzilla::Test::Search).
-use constant FIELD_SIZE => 30;
-
-# These are the custom fields that are created if the BZ_MODIFY_DATABASE_TESTS
-# environment variable is set.
-use constant CUSTOM_FIELDS => {
-    FIELD_TYPE_FREETEXT,  'cf_freetext',
-    FIELD_TYPE_SINGLE_SELECT, 'cf_single_select',
-    FIELD_TYPE_MULTI_SELECT, 'cf_multi_select',
-    FIELD_TYPE_TEXTAREA, 'cf_textarea',
-    FIELD_TYPE_DATETIME, 'cf_datetime',
-    FIELD_TYPE_BUG_ID, 'cf_bugid',
-};
-
-# This translates fielddefs names into Search column names.
-use constant COLUMN_TRANSLATION => {
-    creation_ts => 'opendate',
-    delta_ts    => 'changeddate',
-    work_time => 'actual_time',
-};
-
-# Make comment field names to their Bugzilla::Comment accessor.
-use constant COMMENT_FIELDS => {
-    longdesc  => 'body',
-    commenter => 'author',
-    'longdescs.isprivate' => 'is_private',
-};
-
-# Same as above, for Bugzilla::Attachment.
-use constant ATTACHMENT_FIELDS => {
-    mimetype => 'contenttype',
-    submitter => 'attacher',
-    thedata   => 'data',
-};
-
-# Same, for Bugzilla::Flag.
-use constant FLAG_FIELDS => {
-    'flagtypes.name' => 'name',
-    'setters.login_name' => 'setter',
-    'requestees.login_name' => 'requestee',
-};
-
-# These are fields that we don't test. Test::More will mark these
-# "TODO & SKIP", and not run tests for them at all.
-#
-# We don't support days_elapsed or owner_idle_time yet.
-use constant SKIP_FIELDS => qw(
-    owner_idle_time
-    days_elapsed
-);
-
-# All the fields that represent users.
-use constant USER_FIELDS => qw(
-    assigned_to
-    cc
-    reporter
-    qa_contact
-    commenter
-    attachments.submitter
-    setters.login_name
-    requestees.login_name
-);
-
-# For the "substr"-type searches, how short of a substring should
-# we use? The goal is to be shorter than the full string, but
-# long enough to still be globally unique.
-use constant SUBSTR_SIZE => 20;
-# However, for some fields, we use a different size.
-use constant FIELD_SUBSTR_SIZE => {
-    alias => 11,
-    # Just the month and day.
-    deadline => -5,
-    creation_ts => -8,
-    delta_ts => -8,
-    percentage_complete => 1,
-    work_time => 3,
-    remaining_time => 3,
-    target_milestone => 15,
-    longdesc => 25,
-    # Just the hour and minute.
-    FIELD_TYPE_DATETIME, -5,
-};
-
-# For most fields, we add the length of the name of the field plus
-# the SUBSTR_SIZE specified above to determine how large of a substring
-# we're going to use. However, for some fields, it doesn't make sense to
-# add in their field name this way.
-use constant SUBSTR_NO_FIELD_ADD => FIELD_TYPE_DATETIME, qw(
-    target_milestone remaining_time percentage_complete work_time
-    attachments.mimetype attachments.submitter attachments.filename
-    attachments.description flagtypes.name
-);
-
-################
-# Known Broken #
-################
-
-# See the KNOWN_BROKEN constant for a general description of these
-# "_BROKEN" constants.
-
-# Shared between greaterthan and greaterthaneq.
-#
-# As with other fields, longdescs greaterthan matches if any comment
-# matches (which might be OK).
-#
-# Same for keywords, and cc. Logically, all of these might
-# be OK, but it makes the operation not the logical reverse of
-# lessthaneq. What we're really saying here by marking these broken
-# is that there ought to be some way of searching "all ccs" vs "any cc"
-# (and same for the other fields).
-use constant GREATERTHAN_BROKEN => (
-    cc        => { contains => [1] },
-);
-
-# allwords and allwordssubstr have these broken tests in common.
-use constant ALLWORDS_BROKEN => (
-    # allwordssubstr on cc fields matches against a single cc,
-    # instead of matching against all ccs on a bug.
-    cc        => { contains => [1] },
-    # bug 828344 changed how these searches operate to revert back to the 4.0
-    # behavour, so these tests need to be updated (bug 849117).
-    'flagtypes.name' => { contains => [1] },
-    longdesc         => { contains => [1] },
-);
-
-# Fields that don't generally work at all with changed* searches, but
-# probably should.
-use constant CHANGED_BROKEN => (
-    classification => { contains => [1] },
-    commenter => { contains => [1] },
-    percentage_complete     => { contains => [1] },
-    'requestees.login_name' => { contains => [1] },
-    'setters.login_name'    => { contains => [1] },
-    delta_ts                => { contains => [1] },
-);
-
-# These are additional broken tests that changedfrom and changedto
-# have in common.
-use constant CHANGED_VALUE_BROKEN => (
-    bug_group        => { contains => [1] },
-    cc               => { contains => [1] },
-    estimated_time   => { contains => [1] },
-    'flagtypes.name' => { contains => [1] },
-    keywords  => { contains => [1] },
-    'longdescs.count' => { search => 1 },
-    FIELD_TYPE_MULTI_SELECT, { contains => [1] },
-);
-
-
-# Any test listed in KNOWN_BROKEN gets marked TODO by Test::More
-# (using some complex code in Bugzilla::Test::Seach::FieldTest).
-# This means that if you run the test under "prove -v", these tests will
-# still show up as "not ok", but the test suite results won't show them
-# as a failure.
-#
-# This constant contains operators as keys, which point to hashes. The hashes
-# have field names as keys. Each field name points to a hash describing
-# how that field/operator combination is broken. The "contains"
-# array specifies that that particular "contains" test is expected
-# to fail. If "search" is set to 1, then we expect the creation of the
-# Bugzilla::Search object to fail.
-#
-# To allow handling custom fields, you can also use the field type as a key
-# instead of the field name. Specifying explicit field names always overrides
-# specifying a field type.
-#
-# Sometimes the operators have multiple tests, and one of them works
-# while the other fails. In this case, we have a special override for
-# "operator-value", which uniquely identifies tests.
-use constant KNOWN_BROKEN => {
-    greaterthan   => { GREATERTHAN_BROKEN },
-    greaterthaneq => { GREATERTHAN_BROKEN },
-
-    'allwordssubstr-<1>' => { ALLWORDS_BROKEN },
-    'allwords-<1>' => {
-        ALLWORDS_BROKEN,
-    },
-    'anywords-<1>' => {
-        'flagtypes.name' => { contains => [1,2,3,4,5] },
-    },
-    'anywords-<1> <2>' => {
-        'flagtypes.name' => { contains => [3,4,5] },
-    },
-    'anywordssubstr-<1> <2>' => {
-        'flagtypes.name' => { contains => [3,4,5] },
-    },
-
-    # setters.login_name and requestees.login name aren't tracked individually
-    # in bugs_activity, so can't be searched using this method.
-    #
-    # percentage_complete isn't tracked in bugs_activity (and it would be
-    # really hard to track). However, it adds a 0=0 term instead of using
-    # the changed* charts or simply denying them.
-    #
-    # delta_ts changedbefore/after should probably search for bugs based
-    # on their delta_ts.
-    #
-    # creation_ts changedbefore/after should search for bug creation dates.
-    #
-    # The commenter field changedbefore/after should search for comment
-    # creation dates.
-    #
-    # classification isn't being tracked properly in bugs_activity, I think.
-    #
-    # attach_data.thedata should search when attachments were created and
-    # who they were created by.
-    'changedbefore' => {
-        CHANGED_BROKEN,
-        'attach_data.thedata' => { contains => [1] },
-    },
-    'changedafter' => {
-        'attach_data.thedata' => { contains => [2,3,4] },
-        classification => { contains => [2,3,4] },
-        commenter   => { contains => [2,3,4] },
-        delta_ts    => { contains => [2,3,4] },
-        percentage_complete => { contains => [2,3,4] },
-        'requestees.login_name' => { contains => [2,3,4] },
-        'setters.login_name'    => { contains => [2,3,4] },
-    },
-    changedfrom => {
-        CHANGED_BROKEN,
-        CHANGED_VALUE_BROKEN,
-        # All fields should have a way to search for "changing
-        # from a blank value" probably.
-        blocked   => { contains => [3,4,5], no_criteria => 1 },
-        dependson => { contains => [2,4,5], no_criteria => 1 },
-        work_time => { contains => [1] },
-        FIELD_TYPE_BUG_ID, { contains => [5], no_criteria => 1 },
-    },
-    # changeto doesn't find remaining_time changes (possibly due to us not
-    # tracking that data properly).
-    #
-    # multi-valued fields are stored as comma-separated strings, so you
-    # can't do changedfrom/to on them.
-    #
-    # Perhaps commenter can either tell you who the last commenter was,
-    # or if somebody commented at a given time (combined with other
-    # charts).
-    #
-    # longdesc changedto/from doesn't do anything; maybe it should.
-    # Same for attach_data.thedata.
-    changedto => {
-        CHANGED_BROKEN,
-        CHANGED_VALUE_BROKEN,
-        'attach_data.thedata' => { contains => [1] },
-        longdesc         => { contains => [1] },
-        remaining_time   => { contains => [1] },
-    },
-    changedby => {
-        CHANGED_BROKEN,
-        # This should probably search the attacher or anybody who changed
-        # anything about an attachment at all.
-        'attach_data.thedata' => { contains => [1] },
-        # This should probably search the reporter.
-        creation_ts => { contains => [1] },
-    },
-    notequals => {
-        'flagtypes.name' => { contains => [1, 5] },
-        longdesc         => { contains => [1] },
-    },
-    notregexp => {
-        'flagtypes.name' => { contains => [1, 5] },
-        longdesc         => { contains => [1] },
-    },
-    notsubstring => {
-        'flagtypes.name' => { contains => [5] },
-        longdesc         => { contains => [1] },
-    },
-    nowords => {
-        'flagtypes.name' => { contains => [1, 5] },
-    },
-    nowordssubstr => {
-        'flagtypes.name' => { contains => [5] },
-    },
-};
-
-###################
-# Broken NotTests #
-###################
-
-# Common BROKEN_NOT values for the changed* fields.
-use constant CHANGED_BROKEN_NOT => (
-    "attach_data.thedata"   => { contains => [1] },
-    "classification"        => { contains => [1] },
-    "commenter"             => { contains => [1] },
-    "delta_ts"              => { contains => [1] },
-    percentage_complete     => { contains => [1] },
-    "requestees.login_name" => { contains => [1] },
-    "setters.login_name"    => { contains => [1] },
-);
-
-# For changedfrom and changedto.
-use constant CHANGED_FROM_TO_BROKEN_NOT => (
-    'longdescs.count' => { search => 1 },
-    "bug_group" => { contains => [1] },
-    "cc" => { contains => [1] },
-    "estimated_time" => { contains => [1] },
-    "flagtypes.name" => { contains => [1] },
-    "keywords" => { contains => [1] },    
-    FIELD_TYPE_MULTI_SELECT, { contains => [1] },
-);
-
-# These are field/operator combinations that are broken when run under NOT().
-use constant BROKEN_NOT => {
-    allwords => {
-        cc               => { contains => [1] },
-        'flagtypes.name' => { contains => [1, 5] },
-        longdesc         => { contains => [1] },
-    },
-    'allwords-<1> <2>' => {
-        cc => { },
-    },
-    allwordssubstr => {
-        cc               => { contains => [1] },
-        'flagtypes.name' => { contains => [5, 6] },
-        longdesc         => { contains => [1] },
-    },
-    'allwordssubstr-<1>,<2>' => {
-        cc               => { },
-        longdesc         => { contains => [1] },
-    },
-    anyexact => {
-        'flagtypes.name' => { contains => [1, 2, 5] },
-    },
-    'anywords-<1>' => {
-        'flagtypes.name' => { contains => [1, 2, 3, 4, 5] },
-    },
-    'anywords-<1> <2>' => {
-        'flagtypes.name' => { contains => [3, 4, 5] },
-    },
-    anywordssubstr => {
-        'flagtypes.name' => { contains => [5] },
-    },
-    'anywordssubstr-<1> <2>' => {
-        'flagtypes.name' => { contains => [3,4,5] },
-    },
-    casesubstring => {
-        'flagtypes.name' => { contains => [5] },
-    },
-    changedafter => {
-        "attach_data.thedata"   => { contains => [2, 3, 4] },
-        "classification"        => { contains => [2, 3, 4] },
-        "commenter"             => { contains => [2, 3, 4] },
-        percentage_complete     => { contains => [2, 3, 4] },
-        "delta_ts"              => { contains => [2, 3, 4] },
-        "requestees.login_name" => { contains => [2, 3, 4] },
-        "setters.login_name"    => { contains => [2, 3, 4] },
-    },
-    changedbefore => {
-        CHANGED_BROKEN_NOT,
-    },
-    changedby => {
-        CHANGED_BROKEN_NOT,
-        creation_ts => { contains => [1] },
-        work_time   => { contains => [1] },
-    },
-    changedfrom => {
-        CHANGED_BROKEN_NOT,
-        CHANGED_FROM_TO_BROKEN_NOT,
-        'attach_data.thedata' => { },
-        blocked         => { contains => [1, 2] },
-        dependson       => { contains => [1, 3] },
-        work_time       => { contains => [1] },
-        FIELD_TYPE_BUG_ID, { contains => [1 .. 4] },
-    },
-    changedto => {
-        CHANGED_BROKEN_NOT,
-        CHANGED_FROM_TO_BROKEN_NOT,
-        longdesc => { contains => [1] },
-        "remaining_time" => { contains => [1] },
-    },
-    greaterthan => {
-        cc               => { contains => [1] },
-        'flagtypes.name' => { contains => [5] },
-    },
-    greaterthaneq => {
-        cc               => { contains => [1] },
-        'flagtypes.name' => { contains => [2, 5] },
-    },
-    equals => {
-        'flagtypes.name' => { contains => [1, 5] },
-    },
-    notequals => {
-        longdesc         => { contains => [1] },
-    },
-    notregexp => {
-        longdesc         => { contains => [1] },
-    },
-    notsubstring => {
-        longdesc         => { contains => [1] },
-    },
-    'nowords-<1>' => {
-        'flagtypes.name' => { contains => [5] },
-    },
-    'nowordssubstr-<1>' => {
-        'flagtypes.name' => { contains => [5] },
-    },
-    lessthan => {
-        'flagtypes.name' => { contains => [5] },
-    },
-    lessthaneq => {
-        'flagtypes.name' => { contains => [1, 5] },
-    },
-    regexp => {
-        'flagtypes.name' => { contains => [1, 5] },
-        longdesc         => { contains => [1] },
-    },
-    substring => {
-        'flagtypes.name' => { contains => [5] },
-        longdesc         => { contains => [1] },
-    },
-};
-
-#############
-# Overrides #
-#############
-
-# These overrides are used in the TESTS constant, below.
-
-# Regex tests need unique test values for certain fields.
-use constant REGEX_OVERRIDE => {
-    'attachments.mimetype'  => { value => '^text/x-1-' },
-    bug_file_loc => { value => '^http://1-' },
-    see_also  => { value => '^http://1-' },
-    blocked   => { value => '^<1>$' },
-    dependson => { value => '^<1>$' },
-    bug_id    => { value => '^<1>$' },
-    'attachments.isobsolete' => { value => '^1'},
-    'attachments.ispatch'    => { value => '^1'},
-    'attachments.isprivate'  => { value => '^1' },
-    cclist_accessible        => { value => '^1' },
-    reporter_accessible      => { value => '^1' },
-    everconfirmed            => { value => '^1' },
-    'longdescs.count'        => { value => '^3' },
-    'longdescs.isprivate'    => { value => '^1' },
-    creation_ts => { value => '^2037-01-01' },
-    delta_ts    => { value => '^2037-01-01' },
-    deadline    => { value => '^2037-02-01' },
-    estimated_time => { value => '^1.0' },
-    remaining_time => { value => '^9.0' },
-    work_time      => { value => '^1.0' },
-    longdesc       => { value => '^1-' },
-    percentage_complete => { value => '^10' },
-    FIELD_TYPE_BUG_ID, { value => '^<1>$' },
-    FIELD_TYPE_DATETIME, { value => '^2037-03-01' }
-};
-
-# Common overrides between lessthan and lessthaneq.
-use constant LESSTHAN_OVERRIDE => (
-    alias             => { contains => [1,5] },
-    estimated_time    => { contains => [1,5] },
-    qa_contact        => { contains => [1,5] },
-    resolution        => { contains => [1,5] },
-    status_whiteboard => { contains => [1,5] },
-    FIELD_TYPE_TEXTAREA, { contains => [1,5] },
-    FIELD_TYPE_FREETEXT, { contains => [1,5] },
-);
-
-# The mandatorily-set fields have values higher than <1>,
-# so bug 5 shows up.
-use constant GREATERTHAN_OVERRIDE => (
-    classification => { contains => [2,3,4,5] },
-    assigned_to  => { contains => [2,3,4,5] },
-    bug_id       => { contains => [2,3,4,5] },
-    bug_group    => { contains => [1,2,3,4] },
-    bug_severity => { contains => [2,3,4,5] },
-    bug_status   => { contains => [2,3,4,5] },
-    component    => { contains => [2,3,4,5] },
-    commenter    => { contains => [2,3,4,5] },
-    # keywords matches if *any* keyword matches
-    keywords     => { contains => [1,2,3,4] },
-    longdesc     => { contains => [1,2,3,4] },
-    op_sys       => { contains => [2,3,4,5] },
-    priority     => { contains => [2,3,4,5] },
-    product      => { contains => [2,3,4,5] },
-    reporter     => { contains => [2,3,4,5] },
-    rep_platform => { contains => [2,3,4,5] },
-    short_desc   => { contains => [2,3,4,5] },
-    version      => { contains => [2,3,4,5] },
-    tag          => { contains => [1,2,3,4] },
-    target_milestone => { contains => [2,3,4,5] },
-    # Bug 2 is the only bug besides 1 that has a Requestee set.
-    'requestees.login_name'  => { contains => [2] },
-    FIELD_TYPE_SINGLE_SELECT, { contains => [2,3,4,5] },
-    # Override SINGLE_SELECT for resolution.
-    resolution => { contains => [2,3,4] },
-    # MULTI_SELECTs match if *any* value matches
-    FIELD_TYPE_MULTI_SELECT, { contains => [1,2,3,4] },
-);
-
-# For all positive multi-value types.
-use constant MULTI_BOOLEAN_OVERRIDE => (
-    'attachments.ispatch'    => { value => '1,1', contains => [1] },
-    'attachments.isobsolete' => { value => '1,1', contains => [1] },
-    'attachments.isprivate'  => { value => '1,1', contains => [1] },
-    cclist_accessible        => { value => '1,1', contains => [1] },
-    reporter_accessible      => { value => '1,1', contains => [1] },
-    'longdescs.isprivate'    => { value => '1,1', contains => [1] },
-    everconfirmed            => { value => '1,1', contains => [1] },
-);
-
-# Same as above, for negative multi-value types.
-use constant NEGATIVE_MULTI_BOOLEAN_OVERRIDE => (
-    'attachments.ispatch'    => { value => '1,1', contains => [2,3,4,5] },
-    'attachments.isobsolete' => { value => '1,1', contains => [2,3,4,5] },
-    'attachments.isprivate'  => { value => '1,1', contains => [2,3,4,5] },
-    cclist_accessible        => { value => '1,1', contains => [2,3,4,5] },
-    reporter_accessible      => { value => '1,1', contains => [2,3,4,5] },
-    'longdescs.isprivate'    => { value => '1,1', contains => [2,3,4,5] },
-    everconfirmed            => { value => '1,1', contains => [2,3,4,5] },
-);
-
-# For anyexact and anywordssubstr
-use constant ANY_OVERRIDE => (
-    'longdescs.count' => { contains => [1,2,3,4] },
-    'work_time' => { value => '1.0,2.0' },
-    dependson => { value => '<1>,<3>', contains => [1,3] },
-    MULTI_BOOLEAN_OVERRIDE,
-);
-
-# For all the changed* searches. The ones that have empty contains
-# are fields that never change in value, or will never be rationally
-# tracked in bugs_activity.
-use constant CHANGED_OVERRIDE => (
-    'attachments.submitter' => { contains => [] },
-    bug_id    => { contains => [] },
-    reporter  => { contains => [] },
-    tag       => { contains => [] },
-);
-
-#########
-# Tests #
-#########
-
-# The basic format of this is a hashref, where the keys are operators,
-# and each operator has an arrayref of tests that it runs. The tests
-# are hashrefs, with the following possible keys:
-#
-# contains: This is a list of bug numbers that the search is expected
-#           to contain. (This is bug numbers, like 1,2,3, not the bug
-#           ids. For a description of each bug number, see NUM_BUGS.)
-#           Any bug not listed in "contains" must *not* show up in the
-#           search result.
-# value: The value that you're searching for. There are certain special
-#        codes that will be replaced with bug values when the tests are
-#        run. In these examples below, "#" indicates a bug number:
-#
-#        <#> - The field value for this bug.
-#
-#              For any operator that has the string "word" in it, this is
-#              *all* the values for the current field from the numbered bug,
-#              joined by a space.
-#
-#              If the operator has the string "substr" in it, then we
-#              take a substring of the value (for single-value searches)
-#              or we take a substring of each value and join them (for
-#              multi-value "word" searches). The length of the substring
-#              is determined by the SUBSTR_SIZE constants above.)
-#
-#              For other operators, this just becomes the first value from
-#              the field for the numbered bug.
-#
-#              So, if we were running the "equals" test and checking the
-#              cc field, <1> would become the login name of the first cc on
-#              Bug 1. If we did an "anywords" search test, it would become
-#              a space-separated string of the login names of all the ccs
-#              on Bug 1. If we did an "anywordssubstr" search test, it would
-#              become a space-separated string of the first few characters
-#              of each CC's login name on Bug 1.
-#              
-#        <#-id> - The bug id of the numbered bug.
-#        <#-reporter> - The login name of the numbered bug's reporter.
-#        <#-delta> - The delta_ts of the numbered bug.
-#
-# escape: If true, we will call quotemeta() on the value immediately
-#         before passing it to Search.pm.
-#
-# transform: A function to call on any field value before inserting
-#            it for a <#> replacement. The transformation function
-#            gets all of the bug's values for the field as its arguments.
-# if_equal: This allows you to override "contains" for the case where
-#           the transformed value (from calling the "transform" function)
-#           is equal to the original value.
-#
-# override: This allows you to override "contains" and "values" for
-#           certain fields.
-use constant TESTS => {
-    equals => [
-        { contains => [1], value => '<1>' },
-    ],
-    notequals => [
-        { contains => [2,3,4,5], value => '<1>' },
-    ],
-    substring => [
-        { contains => [1], value => '<1>',
-          override => {
-              percentage_complete => { contains => [1,2,3] },
-          }
-        },
-    ],
-    casesubstring => [
-        { contains => [1], value => '<1>',
-          override => {
-              percentage_complete => { contains => [1,2,3] },
-          }
-        },
-        { contains => [], value => '<1>', transform => sub { lc($_[0]) },
-          extra_name => 'lc', if_equal => { contains => [1] },
-          override => {
-              percentage_complete => { contains => [1,2,3] },
-          }
-        },
-    ],
-    notsubstring => [
-        { contains => [2,3,4,5], value => '<1>',
-          override => {
-              percentage_complete => { contains => [4,5] },
-          },
-        }
-    ],
-    regexp => [
-        { contains => [1], value => '<1>', escape => 1,
-          override => {
-              percentage_complete => { value => '^10' },
-          }
-        },
-        { contains => [1], value => '^1-', override => REGEX_OVERRIDE },
-    ],
-    notregexp => [
-        { contains => [2,3,4,5], value => '<1>', escape => 1,
-          override => {
-              percentage_complete => { value => '^10' },
-          }
-        },
-        { contains => [2,3,4,5], value => '^1-', override => REGEX_OVERRIDE },
-    ],
-    lessthan => [
-        { contains => [1], value => 2, 
-          override => {
-              # A lot of these contain bug 5 because an empty value is validly
-              # less than the specified value.
-              bug_file_loc => { value => 'http://2-', contains => [1,5] },
-              see_also     => { value => 'http://2-' },
-              'attachments.mimetype' => { value => 'text/x-2-' },
-              blocked   => { value => '<4-id>', contains => [1,2] },
-              dependson => { value => '<3-id>', contains => [1,3] },
-              bug_id    => { value => '<2-id>' },
-              'attachments.isprivate'  => { value => 1, contains => [2,3,4] },
-              'attachments.isobsolete' => { value => 1, contains => [2,3,4] },
-              'attachments.ispatch'    => { value => 1, contains => [2,3,4] },
-              cclist_accessible        => { value => 1, contains => [2,3,4,5] },
-              reporter_accessible      => { value => 1, contains => [2,3,4,5] },
-              'longdescs.count'        => { value => 3, contains => [2,3,4,5] },
-              'longdescs.isprivate'    => { value => 1, contains => [1,2,3,4,5] },
-              everconfirmed            => { value => 1, contains => [2,3,4,5] },
-              creation_ts => { value => '2037-01-02', contains => [1,5] },
-              delta_ts    => { value => '2037-01-02', contains => [1,5] },
-              deadline    => { value => '2037-02-02', contains => [1,5] },
-              remaining_time => { value => 10, contains => [1,5] },
-              percentage_complete => { value => 11, contains => [1,5] },
-              longdesc => { value => '2-', contains => [1,5] },
-              work_time => { value => 1, contains => [5] },
-              FIELD_TYPE_BUG_ID, { value => '<2>', contains => [1,5] },
-              FIELD_TYPE_DATETIME, { value => '2037-03-02', contains => [1,5] },
-              LESSTHAN_OVERRIDE,
-          }
-        },
-    ],
-    lessthaneq => [
-        { contains => [1], value => '<1>',
-          override => {
-              'attachments.isobsolete' => { value => 0, contains => [2,3,4] },
-              'attachments.ispatch'    => { value => 0, contains => [2,3,4] },
-              'attachments.isprivate'  => { value => 0, contains => [2,3,4] },
-              cclist_accessible        => { value => 0, contains => [2,3,4,5] },
-              reporter_accessible      => { value => 0, contains => [2,3,4,5] },
-              'longdescs.count'        => { value => 2, contains => [2,3,4,5] },
-              'longdescs.isprivate'    => { value => -1, contains => [] },
-              everconfirmed            => { value => 0, contains => [2,3,4,5] },
-              bug_file_loc   => { contains => [1,5] },
-              blocked        => { contains => [1,2] },
-              deadline       => { contains => [1,5] },
-              dependson      => { contains => [1,3] },
-              creation_ts    => { contains => [1,5] },
-              delta_ts       => { contains => [1,5] },
-              remaining_time => { contains => [1,5] },
-              longdesc       => { contains => [1,5] },
-              percentage_complete => { contains => [1,5] },
-              work_time => { value => 1, contains => [1,5] },
-              FIELD_TYPE_BUG_ID, { contains => [1,5] },
-              FIELD_TYPE_DATETIME, { contains => [1,5] },
-              LESSTHAN_OVERRIDE,
-          },
-        },
-    ],
-    greaterthan => [
-        { contains => [2,3,4], value => '<1>',
-          override => {
-              dependson => { contains => [3] },
-              blocked   => { contains => [2] },
-              'attachments.ispatch'    => { value => 0, contains => [1] },
-              'attachments.isobsolete' => { value => 0, contains => [1] },
-              'attachments.isprivate'  => { value => 0, contains => [1] },
-              cclist_accessible        => { value => 0, contains => [1] },
-              reporter_accessible      => { value => 0, contains => [1] },
-              'longdescs.count'        => { value => 2, contains => [1] },
-              'longdescs.isprivate'    => { value => 0, contains => [1] },
-              everconfirmed            => { value => 0, contains => [1] },
-              'flagtypes.name'         => { value => 2, contains => [2,3,4] },
-              GREATERTHAN_OVERRIDE,
-          },
-        },
-    ],
-    greaterthaneq => [
-        { contains => [2,3,4], value => '<2>',
-          override => {
-              'attachments.ispatch'    => { value => 1, contains => [1] },
-              'attachments.isobsolete' => { value => 1, contains => [1] },
-              'attachments.isprivate'  => { value => 1, contains => [1] },
-              cclist_accessible        => { value => 1, contains => [1] },
-              reporter_accessible      => { value => 1, contains => [1] },
-              'longdescs.count'        => { value => 3, contains => [1] },
-              'longdescs.isprivate'    => { value => 1, contains => [1] },
-              everconfirmed            => { value => 1, contains => [1] },
-              dependson => { value => '<3>', contains => [1,3] },
-              blocked   => { contains => [1,2] },
-              GREATERTHAN_OVERRIDE,
-          }
-        },
-    ],
-    matches => [
-        { contains => [1], value => '<1>' },
-    ],
-    notmatches => [
-        { contains => [2,3,4,5], value => '<1>' },
-    ],
-    anyexact => [
-        { contains => [1,2], value => '<1>, <2>', 
-          override => { ANY_OVERRIDE } },
-    ],
-    anywordssubstr => [
-        { contains => [1,2], value => '<1> <2>', 
-          override => {
-              ANY_OVERRIDE,
-              percentage_complete => { contains => [1,2,3] },
-          }
-        },
-    ],
-    allwordssubstr => [
-        { contains => [1], value => '<1>',
-          override => {
-              MULTI_BOOLEAN_OVERRIDE,
-              # We search just the number "1" for percentage_complete,
-              # which matches a lot of bugs.
-              percentage_complete => { contains => [1,2,3] },
-          },
-        },
-        { contains => [], value => '<1>,<2>',
-          override => {
-              dependson => { value => '<1-id> <3-id>', contains => [] },
-              # bug 3 has the value "21" here, so matches "2,1"
-              percentage_complete => { value => '<2>,<3>', contains => [3] },
-              # 1 0 matches bug 1, which has both public and private comments.
-             'longdescs.isprivate' => { contains => [1] },              
-          }
-        },
-    ],
-    nowordssubstr => [
-        { contains => [2,3,4,5], value => '<1>',
-          override => {
-              # longdescs.isprivate translates to "1 0", so no bugs should
-              # show up.
-              'longdescs.isprivate' => { contains => [] },
-              percentage_complete => { contains => [4,5] },
-              work_time => { contains => [2,3,4,5] },
-          }
-        },
-    ],
-    anywords => [
-        { contains => [1], value => '<1>',
-          override => {
-              MULTI_BOOLEAN_OVERRIDE,
-          }
-        },
-        { contains => [1,2], value => '<1> <2>',
-          override => {
-              MULTI_BOOLEAN_OVERRIDE,
-              dependson => { value => '<1> <3>', contains => [1,3] },
-              'longdescs.count' => { contains => [1,2,3,4] },              
-          },
-        },
-    ],
-    allwords => [
-        { contains => [1], value => '<1>',
-          override => { MULTI_BOOLEAN_OVERRIDE } },
-        { contains => [], value => '<1> <2>',
-          override => {
-            dependson => { contains => [], value => '<2-id> <3-id>' },
-            # 1 0 matches bug 1, which has both public and private comments.
-            'longdescs.isprivate' => { contains => [1] },
-          }
-        },
-    ],
-    nowords => [
-        { contains => [2,3,4,5], value => '<1>',
-          override => {
-              # longdescs.isprivate translates to "1 0", so no bugs should
-              # show up.
-              'longdescs.isprivate' => { contains => [] },
-              work_time => { contains => [2,3,4,5] },
-          }
-        },
-    ],
-
-    changedbefore => [
-        { contains => [1], value => '<1-delta>',
-          override => {
-              CHANGED_OVERRIDE,
-              creation_ts => { contains => [1,5] },
-              blocked   => { contains => [1,2] },
-              dependson => { contains => [1,3] },
-              longdesc => { contains => [1,5] },
-              'longdescs.count' => { contains => [1,5] },
-          }
-        },
-    ],
-    changedafter => [
-        { contains => [2,3,4], value => '<2-delta>',
-          override => { 
-              CHANGED_OVERRIDE,
-              creation_ts => { contains => [3,4] },
-              # We only change this for one bug, and it doesn't match.
-              'longdescs.isprivate' => { contains => [] },
-              # Same for everconfirmed.
-              'everconfirmed' => { contains => [] },
-              # For blocked and dependson, they have the delta_ts of bug1
-              # in the bugs_activity table, so they won't ever match.
-              blocked   => { contains => [] },
-              dependson => { contains => [] },
-          }
-        },
-    ],
-    changedfrom => [
-        { contains => [1], value => '<1>',
-          override => {
-              CHANGED_OVERRIDE,
-              # The test never changes an already-set dependency field, but
-              # we *can* attempt to test searching against an empty value,
-              # which should get us some bugs.
-              blocked   => { value => '', contains => [1,2] },
-              dependson => { value => '', contains => [1,3] },
-              FIELD_TYPE_BUG_ID, { value => '', contains => [1,2,3,4] },
-              # longdesc changedfrom doesn't make any sense.
-              longdesc => { contains => [] },
-              # Nor does creation_ts changedfrom.
-              creation_ts => { contains => [] },
-              'attach_data.thedata' => { contains => [] },
-              bug_id => { value => '<1-id>', contains => [] },
-          },
-        },
-    ],
-    changedto => [
-        { contains => [1], value => '<1>',
-          override => {
-              CHANGED_OVERRIDE,
-              # I can't imagine any use for creation_ts changedto.
-              creation_ts => { contains => [] },
-          }
-        },
-    ],
-    changedby => [
-        { contains => [1], value => '<1-reporter>',
-          override => {
-              CHANGED_OVERRIDE,
-              blocked   => { contains => [1,2] },
-              dependson => { contains => [1,3] },
-          },
-        },
-    ],
-    # XXX these need tests developed
-    isempty => [],
-    isnotempty => [],
-};
-
-# Fields that do not behave as we expect, for InjectionTest.
-# search => 1 means the Bugzilla::Search creation fails.
-# sql_error is a regex that specifies a SQL error that's OK for us to throw.
-# operator_ok overrides the "brokenness" of certain operators, so that they
-# are always OK for that field/operator combination.
-use constant INJECTION_BROKEN_FIELD => {
-    # Pg can't run injection tests against integer or date fields. See bug 577557.
-    'attachments.isobsolete' => { db_skip => ['Pg'] },
-    'attachments.ispatch'    => { db_skip => ['Pg'] },
-    'attachments.isprivate'  => { db_skip => ['Pg'] },
-    blocked                  => { db_skip => ['Pg'] },
-    bug_id                   => { db_skip => ['Pg'] },
-    cclist_accessible        => { db_skip => ['Pg'] },
-    creation_ts              => { db_skip => ['Pg'] },
-    days_elapsed             => { db_skip => ['Pg'] },
-    dependson                => { db_skip => ['Pg'] },
-    deadline                 => { db_skip => ['Pg'] },
-    delta_ts                 => { db_skip => ['Pg'] },
-    estimated_time           => { db_skip => ['Pg'] },
-    everconfirmed            => { db_skip => ['Pg'] },
-    'longdescs.isprivate'    => { db_skip => ['Pg'] },
-    percentage_complete      => { db_skip => ['Pg'] },
-    remaining_time           => { db_skip => ['Pg'] },
-    reporter_accessible      => { db_skip => ['Pg'] },
-    work_time                => { db_skip => ['Pg'] },
-    FIELD_TYPE_BUG_ID,          { db_skip => ['Pg'] },
-    FIELD_TYPE_DATETIME,        { db_skip => ['Pg'] },
-    owner_idle_time => { search => 1 },
-    'longdescs.count' => {
-        search => 1,
-        db_skip => ['Pg'],
-        operator_ok => [qw(allwords allwordssubstr anywordssubstr casesubstring
-                           changedbefore changedafter greaterthan greaterthaneq
-                           lessthan lessthaneq notregexp notsubstring
-                           nowordssubstr regexp substring anywords
-                           notequals nowords equals anyexact)],
-    },
-};
-
-# Operators that do not behave as we expect, for InjectionTest.
-# search => 1 means the Bugzilla::Search creation fails, but
-# field_ok contains fields that it does actually succeed for.
-use constant INJECTION_BROKEN_OPERATOR => {
-    changedafter  => { search => 1, field_ok => ['creation_ts'] },
-    changedbefore => { search => 1, field_ok => ['creation_ts'] },
-    changedby     => { search => 1 },
-    isempty       => { search => 1 },
-    isnotempty    => { search => 1 },
-};
-
-# Tests run by Bugzilla::Test::Search::InjectionTest.
-# We have to make sure the values are all one word or they'll be split
-# up by the multi-word tests.
-use constant INJECTION_TESTS => (
-    { value => ';SEMICOLON_TEST' },
-    { value => '--COMMENT_TEST'  },
-    { value => "'QUOTE_TEST" },
-    { value => "';QUOTE_SEMICOLON_TEST" },
-    { value => '/*STAR_COMMENT_TEST' }
-);
-
-#################
-# Special Tests #
-#################
-
-use constant SPECIAL_PARAM_TESTS => (
-    { field => 'bug_status', operator => 'anyexact', value => '__open__',
-      contains => [5] },
-    { field => 'bug_status', operator => 'anyexact', value => '__closed__',
-      contains => [1,2,3,4] },
-    { field => 'bug_status', operator => 'anyexact', value => '__all__',
-      contains => [1,2,3,4,5] },
-    
-    { field => 'resolution', operator => 'anyexact', value => '---',
-      contains => [5] },
-    
-    # email* query parameters.
-    { field => 'assigned_to', operator => 'anyexact',
-      value => '<1>, <2-reporter>', contains => [1,2],
-      extra_params => { emailreporter1 => 1 } },
-    { field => 'assigned_to', operator => 'equals',
-      value => '<1>', extra_name => 'email2', contains => [],
-      extra_params => {
-          email2 => generate_random_password(100), emaillongdesc2 => 1,
-      },
-    },
-    
-    # standard pronouns
-    { field => 'assigned_to', operator => 'equals', value => '%assignee%',
-      contains => [1,2,3,4,5] },
-    { field => 'reporter', operator => 'equals', value => '%reporter%',
-      contains => [1,2,3,4,5] },
-    { field => 'qa_contact', operator => 'equals', value => '%qacontact%',
-      contains => [1,2,3,4,5] },
-    { field => 'cc', operator => 'equals', value => '%user%',
-      contains => [1] },
-    # group pronouns
-    { field => 'reporter', operator => 'equals',
-      value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] },
-    { field => 'assigned_to', operator => 'equals',
-      value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] },
-    { field => 'qa_contact', operator => 'equals',
-      value => '%group.<1-bug_group>%', contains => [1,2,3,4] },
-    { field => 'cc', operator => 'equals',
-      value => '%group.<1-bug_group>%', contains => [1,2,3,4] },
-    { field => 'commenter', operator => 'equals',
-      value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] },
-);
-
-use constant CUSTOM_SEARCH_TESTS => (
-    { name => 'OP without CP', contains => [1],
-      params => [
-          { f => 'OP' },
-          { f => 'bug_id', o => 'equals', v => '<1>' },
-      ]
-    },
-
-    { name => 'Empty OP/CP pair before criteria', contains => [1],
-      params => [
-          { f => 'OP' }, { f => 'CP' },
-          { f => 'bug_id', o => 'equals', v => '<1>' },
-      ]
-    },
-
-    { name => 'Empty OP/CP pair after criteria', contains => [1],
-      params => [
-          { f => 'bug_id', o => 'equals', v => '<1>' },
-          { f => 'OP' }, { f => 'CP' },
-      ]
-    },
-
-    { name  => 'empty OP/CP mid criteria', contains => [1],
-      columns => ['assigned_to'],
-      params => [
-          { f => 'bug_id', o => 'equals', v => '<1>' },
-          { f => 'OP' }, { f => 'CP' },
-          { f => 'assigned_to', o => 'substr', v => '@' },
-      ]
-    },
-
-    { name  => 'bug_id = 1 AND assigned_to contains @', contains => [1],
-      columns => ['assigned_to'],
-      params => [
-          { f => 'bug_id',      o => 'equals', v => '<1>' },
-          { f => 'assigned_to', o => 'substr', v => '@' },
-      ]
-    },
-
-    { name  => 'NOT(bug_id = 1) AND NOT(assigned_to = 2)',
-      contains => [3,4,5],
-      columns => ['assigned_to'],
-      params => [
-          { n => 1, f => 'bug_id',      o => 'equals', v => '<1>' },
-          { n => 1, f => 'assigned_to', o => 'equals', v => '<2>' },
-      ]
-    },
-
-    { name  => 'bug_id = 1 OR assigned_to = 2', contains => [1,2],
-      columns => ['assigned_to'], top_params => { j_top => 'OR' },
-      params => [
-          { f => 'bug_id',      o => 'equals', v => '<1>' },
-          { f => 'assigned_to', o => 'equals', v => '<2>' },
-      ]
-    },
-
-    { name => 'NOT(bug_id = 1 AND assigned_to = 1)', contains => [2,3,4,5],
-      columns => ['assigned_to'],
-      params => [
-          { f => 'OP', n => 1 },
-            { f => 'bug_id',      o => 'equals', v => '<1>' },
-            { f => 'assigned_to', o => 'equals', v => '<1>' },
-          { f => 'CP' },
-      ]
-    },
-
-
-    { name  => '(bug_id = 1 AND assigned_to contains @) '
-               . ' OR (bug_id = 2 AND assigned_to contains @)',
-      contains => [1,2], columns => ['assigned_to'],
-      top_params => { j_top => 'OR' },
-      params => [
-          { f => 'OP' },
-            { f => 'bug_id',      o => 'equals', v => '<1>' },
-            { f => 'assigned_to', o => 'substr', v => '@' },
-          { f => 'CP' },
-          { f => 'OP' },
-            { f => 'bug_id',      o => 'equals', v => '<2>' },
-            { f => 'assigned_to', o => 'substr', v => '@' },
-          { f => 'CP' },
-      ]
-    },
-
-    { name  => '(bug_id = 1 OR assigned_to = 2) '
-               . ' AND (bug_id = 2 OR assigned_to = 1)',
-      contains => [1,2], columns => ['assigned_to'],
-      params => [
-          { f => 'OP', j => 'OR' },
-            { f => 'bug_id',      o => 'equals', v => '<1>' },
-            { f => 'assigned_to', o => 'equals', v => '<2>' },
-          { f => 'CP' },
-          { f => 'OP', j => 'OR' },
-            { f => 'bug_id',      o => 'equals', v => '<2>' },
-            { f => 'assigned_to', o => 'equals', v => '<1>' },
-          { f => 'CP' },
-      ]
-    },
-
-    { name  => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) '
-               . ' AND (bug_id = 2 OR assigned_to = 1) )',
-      contains => [1,2,3], columns => ['assigned_to'],
-      top_params => { j_top => 'OR' },
-      params => [
-          { f => 'bug_id', o => 'equals', v => '<3>' },
-          { f => 'OP' },
-            { f => 'OP', j => 'OR' },
-              { f => 'bug_id',      o => 'equals', v => '<1>' },
-              { f => 'assigned_to', o => 'equals', v => '<2>' },
-            { f => 'CP' },
-            { f => 'OP', j => 'OR' },
-              { f => 'bug_id',      o => 'equals', v => '<2>' },
-              { f => 'assigned_to', o => 'equals', v => '<1>' },
-            { f => 'CP' },
-          { f => 'CP' },
-      ]
-    },
-
-    { name  => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) '
-               . ' AND (bug_id = 2 OR assigned_to = 1) ) OR bug_id = 4',
-      contains => [1,2,3,4], columns => ['assigned_to'],
-      top_params => { j_top => 'OR' },
-      params => [
-          { f => 'bug_id', o => 'equals', v => '<3>' },
-          { f => 'OP' },
-            { f => 'OP', j => 'OR' },
-              { f => 'bug_id',      o => 'equals', v => '<1>' },
-              { f => 'assigned_to', o => 'equals', v => '<2>' },
-            { f => 'CP' },
-            { f => 'OP', j => 'OR' },
-              { f => 'bug_id',      o => 'equals', v => '<2>' },
-              { f => 'assigned_to', o => 'equals', v => '<1>' },
-            { f => 'CP' },
-          { f => 'CP' },
-          { f => 'bug_id', o => 'equals', v => '<4>' },
-      ]
-    },
-
-);
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/CustomTest.pm b/xt/lib/Bugzilla/Test/Search/CustomTest.pm
deleted file mode 100644 (file)
index 132e5ac..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This module represents a test with custom URL parameters.
-# Tests like this are specified in CUSTOM_SEARCH_TESTS in
-# Bugzilla::Test::Search::Constants.
-package Bugzilla::Test::Search::CustomTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
-use strict;
-use warnings;
-
-use Bugzilla::Test::Search::FieldTest;
-use Bugzilla::Test::Search::OperatorTest;
-
-use Storable qw(dclone);
-
-###############
-# Constructor #
-###############
-
-sub new {
-  my ($class, $test, $search_test) = @_;
-  bless { raw_test => dclone($test), search_test => $search_test }, $class;
-}
-
-#############
-# Accessors #
-#############
-
-sub search_test { return $_[0]->{search_test} }
-sub name { return 'Custom: ' . $_[0]->test->{name} }
-sub test { return $_[0]->{raw_test} }
-
-sub operator_test { die "unimplemented" }
-sub field_object { die "unimplemented" }
-sub main_value { die "unimplenmented" }
-sub test_value { die "unimplemented" }
-# Custom tests don't use transforms.
-sub transformed_value_was_equal { 0 }
-sub debug_value {
-    my ($self) = @_;
-    my $string = '';
-    my $params = $self->search_params;
-    foreach my $param (keys %$params) {
-        $string .= $param . "=" . $params->{$param} . '&';
-    }
-    chop($string);
-    return $string;
-}
-
-# The tests we know are broken for this operator/field combination.
-sub _known_broken { return {} }
-sub contains_known_broken { return undef }
-sub search_known_broken { return undef }
-sub field_not_yet_implemented { return undef }
-sub invalid_field_operator_combination { return undef }
-
-#########################################
-# Accessors: Bugzilla::Search Arguments #
-#########################################
-
-# Converts the f, o, v rows into f0, o0, v0, etc. and translates
-# the values appropriately.
-sub search_params {
-    my ($self) = @_;
-
-    my %params = %{ $self->test->{top_params} || {} };
-    my $counter = 0;
-    foreach my $row (@{ $self->test->{params} }) {
-        $row->{v} = $self->translate_value($row) if exists $row->{v};
-        foreach my $key (keys %$row) {
-            $params{"${key}$counter"} = $row->{$key};
-        }
-        $counter++;
-    }
-
-    return \%params;
-}
-
-sub translate_value {
-    my ($self, $row) = @_;
-    my $as_test = { field => $row->{f}, operator => $row->{o},
-                    value => $row->{v} };
-    my $operator_test = new Bugzilla::Test::Search::OperatorTest($row->{o},
-        $self->search_test);
-    my $field = Bugzilla::Field->check($row->{f});
-    my $field_test = new Bugzilla::Test::Search::FieldTest($operator_test,
-      $field, $as_test);
-    return $field_test->translated_value;
-}
-
-sub search_columns {
-    my ($self) = @_;
-    return ['bug_id', @{ $self->test->{columns} || [] }];
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/FieldTest.pm b/xt/lib/Bugzilla/Test/Search/FieldTest.pm
deleted file mode 100644 (file)
index 5e86d92..0000000
+++ /dev/null
@@ -1,617 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This module represents the tests that get run on a single
-# operator/field combination for Bugzilla::Test::Search.
-# This is where all the actual testing happens.
-package Bugzilla::Test::Search::FieldTest;
-
-use strict;
-use warnings;
-use Bugzilla::Search;
-use Bugzilla::Test::Search::Constants;
-use Bugzilla::Util qw(trim);
-
-use Data::Dumper;
-use Scalar::Util qw(blessed);
-use Test::More;
-use Test::Exception;
-
-###############
-# Constructor #
-###############
-
-sub new {
-    my ($class, $operator_test, $field, $test) = @_;
-    return bless { operator_test => $operator_test,
-                   field_object => $field,
-                   raw_test     => $test }, $class;
-}
-
-#############
-# Accessors #
-#############
-
-sub num_tests { return TESTS_PER_RUN }
-
-# The Bugzilla::Test::Search::OperatorTest that this is a child of.
-sub operator_test { return $_[0]->{operator_test} }
-# The Bugzilla::Field being tested.
-sub field_object { return $_[0]->{field_object} }
-# The name of the field being tested, which we need much more often
-# than we need the object.
-sub field {
-    my ($self) = @_;
-    $self->{field_name} ||= $self->field_object->name;
-    return $self->{field_name};
-}
-# The Bugzilla::Test::Search object that this is a child of.
-sub search_test { return $_[0]->operator_test->search_test }
-# The operator being tested
-sub operator { return $_[0]->operator_test->operator }
-# The bugs currently being tested by Bugzilla::Test::Search.
-sub bugs { return $_[0]->search_test->bugs }
-sub bug {
-    my $self = shift;
-    return $self->search_test->bug(@_);
-}
-sub number {
-    my ($self, $id) = @_;
-    foreach my $number (1..NUM_BUGS) {
-        return $number if $self->search_test->bug($number)->id == $id;
-    }
-    return 0;
-}
-
-# The name displayed for this test by Test::More. Used in test descriptions.
-sub name {
-    my ($self) = @_;
-    my $field = $self->field;
-    my $operator = $self->operator;
-    my $value = $self->main_value;
-    
-    my $name = "$field-$operator-$value";
-    if (my $extra_name = $self->test->{extra_name}) {
-        $name .= "-$extra_name";
-    }
-    return $name;
-}
-
-# The appropriate value from the TESTS constant for this test, taking
-# into account overrides.
-sub test {
-    my $self = shift;
-    return $self->{test} if $self->{test};
-    
-    my %test = %{ $self->{raw_test} };
-    
-    # We have field name overrides...
-    my $override = $test{override}->{$self->field};
-    # And also field type overrides.
-    if (!$override) {
-        $override = $test{override}->{$self->field_object->type} || {};
-    }
-    
-    foreach my $key (%$override) {
-        $test{$key} = $override->{$key};
-    }
-    
-    $self->{test} = \%test;
-    return $self->{test};
-}
-
-# All the values for all the bugs for this field.
-sub _field_values {
-    my ($self) = @_;
-    return $self->{field_values} if $self->{field_values};
-    
-    my %field_values;
-    foreach my $number (1..NUM_BUGS) {
-        $field_values{$number} = $self->_field_values_for_bug($number);
-    }
-    $self->{field_values} = \%field_values;
-    return $self->{field_values};
-}
-# The values for this field for the numbered bug.
-sub bug_values {
-    my ($self, $number) = @_;
-    return @{ $self->_field_values->{$number} };
-}
-
-# The untranslated, non-overriden value--used in the name of the test
-# and other places.
-sub main_value { return $_[0]->{raw_test}->{value} }
-# The untranslated test value, taking into account overrides.
-sub test_value { return $_[0]->test->{value} };
-# The value translated appropriately for passing to Bugzilla::Search.
-sub translated_value {
-    my $self = shift;
-    if (!exists $self->{translated_value}) {
-        my $value = $self->search_test->value_translation_cache($self);
-        if (!defined $value) {
-            $value = $self->_translate_value();
-            $self->search_test->value_translation_cache($self, $value);
-        }
-        $self->{translated_value} = $value;
-    }
-    return $self->{translated_value};
-}
-# Used in failure diagnostic messages.
-sub debug_fail {
-    my ($self, $number, $results, $sql) = @_;
-    my @expected = @{ $self->test->{contains} };
-    my @results = sort
-                  map { $self->number($_) }
-                  map { $_->[0] }
-                  @$results;
-    return
-        "   Value: '" . $self->translated_value . "'\n" .
-        "Expected: [" . join(',', @expected) . "]\n" .
-        " Results: [" . join(',', @results) . "]\n" .
-        trim($sql) . "\n";
-}
-
-# True for a bug if we ran the "transform" function on it and the
-# result was equal to its first value.
-sub transformed_value_was_equal {
-    my ($self, $number, $value) = @_;
-    if (@_ > 2) {
-        $self->{transformed_value_was_equal}->{$number} = $value;
-        $self->search_test->was_equal_cache($self, $number, $value);
-    }
-    my $cached = $self->search_test->was_equal_cache($self, $number);
-    return $cached if defined $cached;
-    return $self->{transformed_value_was_equal}->{$number};
-}
-
-# True if this test is supposed to contain the numbered bug.
-sub bug_is_contained {
-    my ($self, $number) = @_;
-    my $contains = $self->test->{contains};
-    if ($self->transformed_value_was_equal($number)
-        and !$self->test->{override}->{$self->field}->{contains})
-    {
-        $contains = $self->test->{if_equal}->{contains};
-    }
-    return grep($_ == $number, @$contains) ? 1 : 0;
-}
-
-###################################################
-# Accessors: Ways of doing SKIP and TODO on tests #
-###################################################
-
-# The tests we know are broken for this operator/field combination.
-sub _known_broken {
-    my ($self, $constant, $skip_pg_check) = @_;
-
-    $constant ||= KNOWN_BROKEN;
-    my $field = $self->field;
-    my $type = $self->field_object->type;
-    my $operator = $self->operator;
-    my $value = $self->main_value;
-    my $value_name = "$operator-$value";
-    if (my $extra_name = $self->test->{extra_name}) {
-        $value_name .= "-$extra_name";
-    }
-
-    my $value_broken = $constant->{$value_name}->{$field};
-    $value_broken ||= $constant->{$value_name}->{$type};
-    return $value_broken if $value_broken;
-    my $operator_broken = $constant->{$operator}->{$field};
-    $operator_broken ||= $constant->{$operator}->{$type};
-    return $operator_broken if $operator_broken;
-    return {};
-}
-
-# True if the "contains" search for the numbered bug is broken.
-# That is, either the result is supposed to contain it and doesn't,
-# or the result is not supposed to contain it and does.
-sub contains_known_broken {
-    my ($self, $number) = @_;
-    my $field = $self->field;
-    my $operator = $self->operator;
-
-    my $contains_broken = $self->_known_broken->{contains} || [];
-    if (grep($_ == $number, @$contains_broken)) {
-        return "$field $operator contains $number is known to be broken";
-    }
-    return undef;
-}
-
-# Used by subclasses. Checks both bug_is_contained and contains_known_broken
-# to tell you whether or not the bug will *actually* be found by the test.
-sub will_actually_contain_bug {
-    my ($self, $number) = @_;
-    my $is_contained = $self->bug_is_contained($number) ? 1 : 0;
-    my $is_broken = $self->contains_known_broken($number) ? 1 : 0;
-
-    # If the test is supposed to contain the bug and *isn't* broken,
-    # then the test will contain the bug.
-    return 1 if ($is_contained and !$is_broken);
-    # If this test is *not* supposed to contain the bug, but that test is
-    # broken, then this test *will* contain the bug.
-    return 1 if (!$is_contained and $is_broken);
-
-    return 0;
-}
-
-# Returns a string if creating a Bugzilla::Search object throws an error,
-# with this field/operator/value combination.
-sub search_known_broken {
-    my ($self) = @_;
-    my $field = $self->field;
-    my $operator = $self->operator;
-    if ($self->_known_broken->{search}) {
-        return "Bugzilla::Search for $field $operator is known to be broken";
-    }
-    return undef;
-}
-    
-# Returns a string if we haven't yet implemented the tests for this field,
-# but we plan to in the future.
-sub field_not_yet_implemented {
-    my ($self) = @_;
-    my $skip_this_field = grep { $_ eq $self->field } SKIP_FIELDS;
-    if ($skip_this_field) {
-        my $field = $self->field;
-        return "$field testing not yet implemented";
-    }
-    return undef;
-}
-
-# Returns a message if this field/operator combination can't ever be run.
-# At no time in the future will this field/operator combination ever work.
-sub invalid_field_operator_combination {
-    my ($self) = @_;
-    my $field = $self->field;
-    my $operator = $self->operator;
-    
-    if ($field eq 'content' && $operator !~ /matches/) {
-        return "content field does not support $operator";
-    }
-    elsif ($operator =~ /matches/ && $field ne 'content') {
-        return "matches operator does not support fields other than content";
-    }
-    return undef;
-}
-
-# True if this field is broken in an OR combination.
-sub join_broken {
-    my ($self, $or_broken_map) = @_;
-    my $or_broken = $or_broken_map->{$self->field . '-' . $self->operator};
-    if (!$or_broken) {
-        # See if this is a comment field, and in that case, if there's
-        # a generic entry for all comment fields.
-        my $is_comment_field = COMMENT_FIELDS->{$self->field};
-        if ($is_comment_field) {
-            $or_broken = $or_broken_map->{'longdescs.-' . $self->operator};
-        }
-    }
-    return $or_broken;
-}
-
-#########################################
-# Accessors: Bugzilla::Search Arguments #
-#########################################
-
-# The data that will get passed to Bugzilla::Search as its arguments.
-sub search_params {
-    my ($self) = @_;
-    return $self->{search_params} if $self->{search_params};
-
-    my %params = (
-        "field0-0-0" => $self->field,
-        "type0-0-0"  => $self->operator,
-        "value0-0-0"  => $self->translated_value,
-    );
-    
-    $self->{search_params} = \%params;
-    return $self->{search_params};
-}
-
-sub search_columns {
-    my ($self) = @_;
-    my $field = $self->field;
-    my @search_fields = qw(bug_id);
-    if ($self->field_object->buglist) {
-        my $col_name = COLUMN_TRANSLATION->{$field} || $field;
-        push(@search_fields, $col_name);
-    }
-    return \@search_fields;
-}
-
-
-################
-# Field Values #
-################
-
-sub _field_values_for_bug {
-    my ($self, $number) = @_;
-    my $field = $self->field;
-
-    my @values;
-
-    if ($field =~ /^attach.+\.(.+)$/ ) {
-        my $attach_field = $1;
-        $attach_field = ATTACHMENT_FIELDS->{$attach_field} || $attach_field;
-        @values = $self->_values_for($number, 'attachments', $attach_field);
-    }
-    elsif (my $flag_field = FLAG_FIELDS->{$field}) {
-        @values = $self->_values_for($number, 'flags', $flag_field);
-    }
-    elsif (my $translation = COMMENT_FIELDS->{$field}) {
-        @values = $self->_values_for($number, 'comments', $translation);
-        # We want the last value to come first, so that single-value
-        # searches use the last comment.
-        @values = reverse @values;
-    }
-    elsif ($field eq 'longdescs.count') {
-        @values = scalar(@{ $self->bug($number)->comments });
-    }
-    elsif ($field eq 'work_time') {
-        @values = $self->_values_for($number, 'actual_time');
-    }
-    elsif ($field eq 'bug_group') {
-        @values = $self->_values_for($number, 'groups_in', 'name');
-    }
-    elsif ($field eq 'keywords') {
-        @values = $self->_values_for($number, 'keyword_objects', 'name'); 
-    }
-    elsif ($field eq 'content') {
-        @values = $self->_values_for($number, 'short_desc');
-    }
-    elsif ($field eq 'see_also') {
-        @values = $self->_values_for($number, 'see_also', 'name');
-    }
-    elsif ($field eq 'tag') {
-        @values = $self->_values_for($number, 'tags');
-    }
-    # Bugzilla::Bug truncates creation_ts, but we need the full value
-    # from the database. This has no special value for changedfrom,
-    # because it never changes.
-    elsif ($field eq 'creation_ts') {
-        my $bug = $self->bug($number);
-        my $creation_ts = Bugzilla->dbh->selectrow_array(
-            'SELECT creation_ts FROM bugs WHERE bug_id = ?',
-            undef, $bug->id);
-        @values = ($creation_ts);
-    }
-    else {
-        @values = $self->_values_for($number, $field);
-    }
-
-    # We convert user objects to their login name, here, all in one
-    # block for simplicity.
-    if (grep { $_ eq $field } USER_FIELDS) {
-        # requestees.login_name is empty for most bugs (but checking
-        # blessed(undef) handles that.
-        # Values that come from %original_values aren't User objects.
-        @values = map { blessed($_) ? $_->login : $_ } @values;
-        @values = grep { defined $_ } @values;
-    }
-    
-    return \@values;
-}
-
-sub _values_for {
-    my ($self, $number, $bug_field, $item_field) = @_;
-
-    my $item;
-    if ($self->operator eq 'changedfrom') {
-        $item = $self->search_test->bug_create_value($number, $bug_field);
-    }
-    else {
-        my $bug = $self->bug($number);
-        $item = $bug->$bug_field;
-    }
-
-    if ($item_field) {
-        if ($bug_field eq 'flags' and $item_field eq 'name') {
-            return (map { $_->name . $_->status } @$item);
-        }
-        return (map { $self->_get_item($_, $item_field) } @$item);
-    }
-
-    return @$item if ref($item) eq 'ARRAY';
-    return $item if defined $item;
-    return ();
-}
-
-sub _get_item {
-    my ($self, $from, $field) = @_;
-    if (blessed($from)) {
-        return $from->$field;
-    }
-    return $from->{$field};
-}
-
-#####################
-# Value Translation #
-#####################
-
-# This function translates the "value" specified in TESTS into an actual
-# search value to pass to Search.pm. This means that we get the value
-# from the current bug (or, in the case of changedfrom, from %original_values)
-# and then we insert it as required into the "value" from TESTS. (For example,
-# <1> becomes the value for the field from bug 1.)
-sub _translate_value {
-    my $self = shift;
-    my $value = $self->test_value;
-    foreach my $number (1..NUM_BUGS) {
-        $value = $self->_translate_value_for_bug($number, $value);
-    }
-    # Sanity check to make sure that none of the <> stuff was left in.
-    if ($value =~ /<\d/) {
-        die $self->name . ": value untranslated: $value\n";
-    }
-    return $value;
-}
-
-sub _translate_value_for_bug {
-    my ($self, $number, $value) = @_;
-    
-    my $bug = $self->bug($number);
-    
-    my $bug_id = $bug->id;
-    $value =~ s/<$number-id>/$bug_id/g;
-    my $bug_delta = $bug->delta_ts;
-    $value =~ s/<$number-delta>/$bug_delta/g;
-    my $reporter = $bug->reporter->login;
-    $value =~ s/<$number-reporter>/$reporter/g;
-    if ($value =~ /<$number-bug_group>/) {
-        my @bug_groups = map { $_->name } @{ $bug->groups_in };
-        @bug_groups = grep { $_ =~ /^\d+-group-/ } @bug_groups;
-        my $group = $bug_groups[0];
-        $value =~ s/<$number-bug_group>/$group/g;
-    }
-    
-    my @bug_values = $self->bug_values($number);    
-    return $value if !@bug_values;
-    
-    if ($self->operator =~ /substr/) {
-        @bug_values = map { $self->_substr_value($_) } @bug_values;
-    }
-
-    my $string_value = $bug_values[0];
-    if ($self->operator =~ /word/) {
-        $string_value = join(' ', @bug_values);
-    }
-    if (my $func = $self->test->{transform}) {
-        my $transformed = $func->(@bug_values);
-        my $is_equal = $transformed eq $bug_values[0] ? 1 : 0;
-        $self->transformed_value_was_equal($number, $is_equal);
-        $string_value = $transformed;
-    }
-
-    if ($self->test->{escape}) {
-        $string_value = quotemeta($string_value);
-    }
-    $value =~ s/<$number>/$string_value/g;
-    
-    return $value;
-}
-
-sub _substr_value {
-    my ($self, $value) = @_;
-    my $field = $self->field;
-    my $type  = $self->field_object->type;
-    my $substr_size = SUBSTR_SIZE;
-    if (exists FIELD_SUBSTR_SIZE->{$field}) {
-        $substr_size = FIELD_SUBSTR_SIZE->{$field};
-    }
-    elsif (exists FIELD_SUBSTR_SIZE->{$type}) {
-        $substr_size = FIELD_SUBSTR_SIZE->{$type};
-    }
-    if ($substr_size > 0) {
-        # The field name is included in every field value, and if it's
-        # long, it might take up the whole substring, and we don't want that.
-        if (!grep { $_ eq $field or $_ eq $type } SUBSTR_NO_FIELD_ADD) {
-            $substr_size += length($field);
-        }
-        my $string = substr($value, 0, $substr_size);
-        return $string;
-    }
-    return substr($value, $substr_size);
-}
-
-#####################
-# Main Test Methods #
-#####################
-
-sub run {
-    my ($self) = @_;
-    
-    my $invalid_combination = $self->invalid_field_operator_combination;
-    my $field_not_implemented = $self->field_not_yet_implemented;
-
-    SKIP: {    
-        skip($invalid_combination, $self->num_tests) if $invalid_combination;
-        TODO: {
-            todo_skip ($field_not_implemented, $self->num_tests) if $field_not_implemented;
-            $self->do_tests();
-        }
-    }
-}
-
-sub do_tests {
-    my ($self) = @_;
-    my $name = $self->name;
-
-    my $search_broken = $self->search_known_broken;
-    
-    my $search = $self->_test_search_object_creation();
-
-    my $sql;
-    TODO: {
-        local $TODO = $search_broken if $search_broken;
-        lives_ok { $sql = $search->_sql } "$name: generate SQL";
-    }
-    
-    my $results;
-    SKIP: {
-        skip "Can't run SQL without any SQL", 1 if !defined $sql;
-        $results = $self->_test_sql($search);
-    }
-
-    $self->_test_content($results, $sql);
-}
-
-sub _test_search_object_creation {
-    my ($self) = @_;
-    my $name = $self->name;
-    my @args = (fields => $self->search_columns, params => $self->search_params);
-    my $search;
-    lives_ok { $search = new Bugzilla::Search(@args) }
-             "$name: create search object";
-    return $search;
-}
-
-sub _test_sql {
-    my ($self, $search) = @_;
-    my $name = $self->name;
-    my $results;
-    lives_ok { $results = $search->data } "$name: Run SQL Query"
-        or diag($search->_sql);
-    return $results;
-}
-
-sub _test_content {
-    my ($self, $results, $sql) = @_;
-
-    SKIP: {
-        skip "Without results we can't test them", NUM_BUGS if !$results;
-        foreach my $number (1..NUM_BUGS) {
-            $self->_test_content_for_bug($number, $results, $sql);
-        }
-    }
-}
-
-sub _test_content_for_bug {
-    my ($self, $number, $results, $sql) = @_;
-    my $name = $self->name;
-    
-    my $contains_known_broken = $self->contains_known_broken($number);
-    
-    my %result_ids = map { $_->[0] => 1 } @$results;
-    my $bug_id = $self->bug($number)->id;
-    
-    TODO: {
-        local $TODO = $contains_known_broken if $contains_known_broken;
-        if ($self->bug_is_contained($number)) {
-            ok($result_ids{$bug_id},
-               "$name: contains bug $number ($bug_id)")
-                or diag $self->debug_fail($number, $results, $sql);
-        }
-        else {
-            ok(!$result_ids{$bug_id},
-               "$name: does not contain bug $number ($bug_id)")
-                or diag $self->debug_fail($number, $results, $sql);
-        }
-    }
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm b/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm
deleted file mode 100644 (file)
index 888e7eb..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This is the same as a FieldTest, except that it uses normal URL
-# parameters instead of Boolean Charts.
-package Bugzilla::Test::Search::FieldTestNormal;
-use strict;
-use warnings;
-use parent qw(Bugzilla::Test::Search::FieldTest);
-
-use Scalar::Util qw(blessed);
-
-use constant CH_OPERATOR => {
-    changedafter  => 'chfieldfrom',
-    changedbefore => 'chfieldto',
-    changedto     => 'chfieldvalue',
-};
-
-use constant EMAIL_FIELDS => qw(assigned_to qa_contact cc reporter commenter);
-
-# Normally, we just clone a FieldTest because that's the best for performance,
-# overall--that way we don't have to translate the value again. However,
-# sometimes (like in Bugzilla::Test::Search's direct code) we just want
-# to create a FieldTestNormal.
-sub new {
-    my $class = shift;
-    my ($first_arg) = @_;
-    if (blessed $first_arg
-        and $first_arg->isa('Bugzilla::Test::Search::FieldTest'))
-    {
-        my $self = { %$first_arg };
-        return bless $self, $class;
-    }
-    return $class->SUPER::new(@_);
-}
-
-sub name {
-    my $self = shift;
-    my $name = $self->SUPER::name(@_);
-    return "$name (Normal Params)";
-}
-
-sub search_columns {
-    my $self = shift;
-    my $field = $self->field;
-    # For the assigned_to, qa_contact, and reporter fields, have the
-    # "Normal Params" test check that the _realname columns work
-    # all by themselves.
-    if (grep($_ eq $field, EMAIL_FIELDS) && $self->field_object->buglist) {
-        return ['bug_id', "${field}_realname"]
-    }
-    return $self->SUPER::search_columns(@_);
-}
-
-sub search_params {
-    my ($self) = @_;
-    my $field = $self->field;
-    my $operator = $self->operator;
-    my $value = $self->translated_value;
-    if ($operator eq 'anyexact') {
-        $value = [split ',', $value];
-    }
-    
-    if (my $ch_param = CH_OPERATOR->{$operator}) {
-        if ($field eq 'creation_ts') {
-            $field = '[Bug creation]';
-        }
-        return { chfield => $field, $ch_param => $value };
-    }
-    
-    if ($field eq 'delta_ts' and $operator eq 'greaterthaneq') {
-        return { chfieldfrom => $value };
-    }
-    if ($field eq 'delta_ts' and $operator eq 'lessthaneq') {
-        return { chfieldto => $value };
-    }
-    
-    if ($field eq 'deadline' and $operator eq 'greaterthaneq') {
-        return { deadlinefrom => $value };
-    }
-    if ($field eq 'deadline' and $operator eq 'lessthaneq') {
-        return { deadlineto => $value };
-    }
-    
-    if (grep { $_ eq $field } EMAIL_FIELDS) {
-        $field = 'longdesc' if $field eq 'commenter';
-        return {
-            email1           => $value,
-            "email${field}1" => 1,
-            emailtype1       => $operator,
-            # Used to do extra tests on special sorts of email* combinations.
-            %{ $self->test->{extra_params} || {} },
-        };
-    }
-
-    $field =~ s/\./_/g;
-    return { $field => $value, "${field}_type" => $operator };
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm b/xt/lib/Bugzilla/Test/Search/InjectionTest.pm
deleted file mode 100644 (file)
index 90eaabc..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This module represents the SQL Injection tests that get run on a single
-# operator/field combination for Bugzilla::Test::Search.
-package Bugzilla::Test::Search::InjectionTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
-
-use strict;
-use warnings;
-use Bugzilla::Test::Search::Constants;
-use Test::Exception;
-
-sub num_tests { return NUM_SEARCH_TESTS }
-
-sub _known_broken {
-    my ($self) = @_;
-    my $operator_broken = INJECTION_BROKEN_OPERATOR->{$self->operator};
-    # We don't want to auto-vivify $operator_broken and thus make it true.
-    my @field_ok = $operator_broken ? @{ $operator_broken->{field_ok} || [] }
-                                    : ();
-    $operator_broken = undef if grep { $_ eq $self->field } @field_ok;
-
-    my $field_broken = INJECTION_BROKEN_FIELD->{$self->field}
-                       || INJECTION_BROKEN_FIELD->{$self->field_object->type};
-    # We don't want to auto-vivify $field_broken and thus make it true.
-    my @operator_ok = $field_broken ? @{ $field_broken->{operator_ok} || [] }
-                                    : ();
-    $field_broken = undef if grep { $_ eq $self->operator } @operator_ok;
-
-    return $operator_broken || $field_broken || {};
-}
-
-sub sql_error_ok { return $_[0]->_known_broken->{sql_error} }
-
-# Injection tests only skip fields on certain dbs.
-sub field_not_yet_implemented {
-    my ($self) = @_;
-    # We use the constant directly because we don't want operator_ok
-    # or field_ok to stop us.
-    my $broken = INJECTION_BROKEN_FIELD->{$self->field}
-                 || INJECTION_BROKEN_FIELD->{$self->field_object->type};
-    my $skip_for_dbs = $broken->{db_skip};
-    return undef if !$skip_for_dbs;
-    my $dbh = Bugzilla->dbh;
-    if (my ($skip) = grep { $dbh->isa("Bugzilla::DB::$_") } @$skip_for_dbs) {
-        my $field = $self->field;
-        return "$field injection testing is not supported with $skip";
-    }
-    return undef;
-}
-# Injection tests don't do translation.
-sub translated_value { $_[0]->test_value }
-
-sub name { return "injection-" . $_[0]->SUPER::name; }
-
-# Injection tests don't check content.
-sub _test_content {}
-
-sub _test_sql {
-    my $self = shift;
-    my ($sql) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $name = $self->name;
-    if (my $error_ok = $self->sql_error_ok) {
-        throws_ok { $dbh->selectall_arrayref($sql) } $error_ok,
-                  "$name: SQL query dies, as we expect";
-        return;
-    }
-    return $self->SUPER::_test_sql(@_);
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/NotTest.pm b/xt/lib/Bugzilla/Test/Search/NotTest.pm
deleted file mode 100644 (file)
index 190b856..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This module runs tests just like a normal FieldTest, AndTest,
-# or OrTest, but in a NOT chart instead of a normal chart.
-#
-# Logically this should be a mixin of some sort so that we can apply
-# it to OrTest and AndTest, but without Moose there isn't much of an
-# easy way to do that.
-package Bugzilla::Test::Search::NotTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
-use strict;
-use warnings;
-use Bugzilla::Test::Search::Constants;
-
-# We just clone a FieldTest because that's the best for performance,
-# overall--that way we don't have to translate the value again.
-sub new {
-    my ($class, $field_test) = @_;
-    my $self = { %$field_test };
-    return bless $self, $class;
-}
-
-#############
-# Accessors #
-#############
-
-sub name {
-    my ($self) = @_;
-    return "NOT(" . $self->SUPER::name . ")";
-}
-
-# True if this test is supposed to contain the numbered bug. Reversed for
-# NOT tests.
-sub bug_is_contained {
-    my $self = shift;
-    my ($number) = @_;
-    # No search ever returns bug 6, because it's protected by security groups
-    # that the searcher isn't a member of.
-    return 0 if $number == 6;
-    return $self->SUPER::bug_is_contained(@_) ? 0 : 1;
-}
-
-# NOT tests have their own constant for tracking broken-ness.
-sub _known_broken {
-    my ($self) = @_;
-    return $self->SUPER::_known_broken(BROKEN_NOT, 'skip pg check');
-}
-
-sub search_params {
-    my ($self) = @_;
-    my %params = %{ $self->SUPER::search_params() };
-    $params{negate0} = 1;
-    return \%params;
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/OperatorTest.pm b/xt/lib/Bugzilla/Test/Search/OperatorTest.pm
deleted file mode 100644 (file)
index 5ab502d..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This module represents the tests that get run on a single operator
-# from the TESTS constant in Bugzilla::Search::Test::Constants.
-package Bugzilla::Test::Search::OperatorTest;
-
-use strict;
-use warnings;
-use Bugzilla::Test::Search::Constants;
-use Bugzilla::Test::Search::FieldTest;
-use Bugzilla::Test::Search::FieldTestNormal;
-use Bugzilla::Test::Search::InjectionTest;
-use Bugzilla::Test::Search::OrTest;
-use Bugzilla::Test::Search::AndTest;
-use Bugzilla::Test::Search::NotTest;
-
-###############
-# Constructor #
-###############
-
-sub new {
-    my ($invocant, $operator, $search_test) = @_;
-    $search_test ||= $invocant->search_test;
-    my $class = ref($invocant) || $invocant;
-    return bless { search_test => $search_test, operator => $operator }, $class;
-}
-
-#############
-# Accessors #
-#############
-
-# The Bugzilla::Test::Search object that this is a child of.
-sub search_test { return $_[0]->{search_test} }
-# The operator being tested
-sub operator { return $_[0]->{operator} }
-# The tests that we're going to run on this operator.
-sub tests { return @{ TESTS->{$_[0]->operator } } }
-# The fields we're going to test for this operator.
-sub test_fields { return $_[0]->search_test->all_fields }
-
-sub run {
-    my ($self) = @_;
-
-    foreach my $field ($self->test_fields) {
-        foreach my $test ($self->tests) {
-            my $field_test =
-                new Bugzilla::Test::Search::FieldTest($self, $field, $test);
-            $field_test->run();
-            my $normal_test =
-                new Bugzilla::Test::Search::FieldTestNormal($field_test);
-            $normal_test->run();
-            my $not_test = new Bugzilla::Test::Search::NotTest($field_test);
-            $not_test->run();
-            
-            next if !$self->search_test->option('long');
-
-            # Run the OR tests. This tests every other operator (including
-            # this operator itself) in combination with every other field,
-            # in an OR with this operator and field.
-            foreach my $other_operator ($self->search_test->all_operators) {
-                $self->run_join_tests($field_test, $other_operator);
-            }
-        }
-        foreach my $test (INJECTION_TESTS) {
-            my $injection_test =
-                new Bugzilla::Test::Search::InjectionTest($self, $field, $test);
-            $injection_test->run();
-        }
-    }
-}
-
-sub run_join_tests {
-    my ($self, $field_test, $other_operator) = @_;
-
-    my $other_operator_test = $self->new($other_operator);
-    foreach my $other_test ($other_operator_test->tests) {
-        foreach my $other_field ($self->test_fields) {
-            $self->_run_one_join_test($field_test, $other_operator_test,
-                                      $other_field, $other_test);
-            $self->search_test->clean_test_history();
-        }
-    }
-}
-
-sub _run_one_join_test {
-    my ($self, $field_test, $other_operator_test, $other_field, $other_test) = @_;
-    my $other_field_test =
-        new Bugzilla::Test::Search::FieldTest($other_operator_test,
-                                              $other_field, $other_test);
-    my $or_test = new Bugzilla::Test::Search::OrTest($field_test,
-                                                     $other_field_test);
-    $or_test->run();
-    my $and_test = new Bugzilla::Test::Search::AndTest($field_test,
-                                                       $other_field_test);
-    $and_test->run();
-}
-
-1;
diff --git a/xt/lib/Bugzilla/Test/Search/OrTest.pm b/xt/lib/Bugzilla/Test/Search/OrTest.pm
deleted file mode 100644 (file)
index 1b948f3..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# This test combines two field/operator combinations using OR in
-# a single boolean chart.
-package Bugzilla::Test::Search::OrTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
-
-use Bugzilla::Test::Search::Constants;
-use List::MoreUtils qw(all any uniq);
-
-use constant type => 'OR';
-
-###############
-# Constructor #
-###############
-
-sub new {
-    my $class = shift;
-    my $self = { field_tests => [@_] };
-    return bless $self, $class;
-}
-
-#############
-# Accessors #
-#############
-
-sub field_tests { return @{ $_[0]->{field_tests} } }
-sub search_test { ($_[0]->field_tests)[0]->search_test }
-
-sub name {
-    my ($self) = @_;
-    my @names = map { $_->name } $self->field_tests;
-    return join('-' . $self->type . '-', @names);
-}
-
-# In an OR test, bugs ARE supposed to be contained if they are contained
-# by ANY test.
-sub bug_is_contained {
-    my ($self, $number) = @_;
-    return any { $_->bug_is_contained($number) } $self->field_tests;
-}
-
-# Needed only for failure messages
-sub debug_value {
-    my ($self) = @_;
-    my @values = map { $_->field . ' ' . $_->debug_value } $self->field_tests;
-    return join(' ' . $self->type . ' ', @values);
-}
-
-########################
-# SKIP & TODO Messages #
-########################
-
-sub field_not_yet_implemented {
-    my ($self) = @_;
-    return $self->_join_messages('field_not_yet_implemented');
-}
-sub invalid_field_operator_combination {
-    my ($self) = @_;
-    return $self->_join_messages('invalid_field_operator_combination');
-}
-sub search_known_broken {
-    my ($self) = @_;
-    return $self->_join_messages('search_known_broken');    
-}
-
-sub _join_messages {
-    my ($self, $message_method) = @_;
-    my @messages = map { $_->$message_method } $self->field_tests;
-    @messages = grep { $_ } @messages;
-    return join(' AND ', @messages);
-}
-
-sub _bug_will_actually_be_contained {
-    my ($self, $number) = @_;
-
-    foreach my $test ($self->field_tests) {
-        # Some tests are broken in such a way that they actually
-        # generate no criteria in the SQL. In this case, the only way
-        # the test contains the bug is if *another* test contains it.
-        next if $test->_known_broken->{no_criteria};
-        return 1 if $test->will_actually_contain_bug($number);
-    }
-    return 0;
-}
-
-sub contains_known_broken {
-    my ($self, $number) = @_;
-
-    if ( ( $self->bug_is_contained($number)
-           and !$self->_bug_will_actually_be_contained($number) )
-        or ( !$self->bug_is_contained($number)
-             and $self->_bug_will_actually_be_contained($number) ) )
-    {
-        my @messages = map { $_->contains_known_broken($number) } 
-                           $self->field_tests;
-        @messages = grep { $_ } @messages;
-        # Sometimes, with things that break because of no_criteria, there won't
-        # be anything in @messages even though we need to print out a message.
-        if (!@messages) {
-            my @no_criteria = grep { $_->_known_broken->{no_criteria} }
-                                   $self->field_tests;
-            @messages = map { "No criteria generated by " . $_->name }
-                            @no_criteria;
-        }
-        die "broken test with no message" if !@messages;
-        return join(' AND ', @messages);
-    }
-    return undef;
-}
-
-##############################
-# Bugzilla::Search arguments #
-##############################
-
-sub search_columns {
-    my ($self) = @_;
-    my @columns = map { @{ $_->search_columns } } $self->field_tests;
-    return [uniq @columns];
-}
-
-sub search_params {
-    my ($self) = @_;
-    my @all_params = map { $_->search_params } $self->field_tests;
-    my %params;
-    my $chart = 0;
-    foreach my $item (@all_params) {
-        $params{"field0-0-$chart"} = $item->{'field0-0-0'};
-        $params{"type0-0-$chart"}  = $item->{'type0-0-0'};
-        $params{"value0-0-$chart"} = $item->{'value0-0-0'};
-        $chart++;
-    }
-    return \%params;
-}
-
-1;
diff --git a/xt/lib/QA/REST.pm b/xt/lib/QA/REST.pm
new file mode 100644 (file)
index 0000000..4de9856
--- /dev/null
@@ -0,0 +1,65 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package QA::REST;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use autodie;
+
+use LWP::UserAgent;
+use JSON;
+use QA::Util;
+
+use parent qw(LWP::UserAgent Exporter);
+
+@QA::REST::EXPORT = qw(
+    MUST_FAIL
+    get_rest_client
+);
+
+use constant MUST_FAIL => 1;
+
+sub get_rest_client {
+    my $rest_client = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } );
+    bless($rest_client, 'QA::REST');
+    my $config = $rest_client->{bz_config} = get_config();
+    $rest_client->{bz_url} = $config->{browser_url} . '/' . $config->{bugzilla_installation} . '/rest/';
+    $rest_client->{bz_default_headers} = {'Accept' => 'application/json', 'Content-Type' => 'application/json'};
+    return $rest_client;
+}
+
+sub bz_config { return $_[0]->{bz_config}; }
+
+sub call {
+    my ($self, $method, $data, $http_verb, $expect_to_fail) = @_;
+    $http_verb = lc($http_verb || 'GET');
+    $data //= {};
+
+    my %args = %{ $self->{bz_default_headers} };
+    # We do not pass the API key in the URL, so that it's not logged by the web server.
+    if ($http_verb eq 'get' && $data->{api_key}) {
+        $args{'X-BUGZILLA-API-KEY'} = $data->{api_key};
+    }
+    elsif ($http_verb ne 'get') {
+        $args{Content} = encode_json($data);
+    }
+
+    my $response = $self->$http_verb($self->{bz_url} . $method, %args);
+    my $res = decode_json($response->decoded_content);
+    if ($response->is_success xor $expect_to_fail) {
+        return $res;
+    }
+    else {
+        die 'error ' . $res->{code} . ': ' . $res->{message} . "\n";
+    }
+}
diff --git a/xt/lib/QA/RPC.pm b/xt/lib/QA/RPC.pm
new file mode 100644 (file)
index 0000000..63a7d95
--- /dev/null
@@ -0,0 +1,289 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::RPC;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use Data::Dumper;
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER create_bug_fields);
+use Storable qw(dclone);
+use Test::More;
+
+sub bz_config {
+    my $self = shift;
+    $self->{bz_config} ||= QA::Util::get_config();
+    return $self->{bz_config};
+}
+
+# True if we're doing calls over GET instead of POST.
+sub bz_get_mode { return 0 }
+
+# When doing bz_log_in over GET, we can't actually call User.login,
+# we just store credentials here and then pass them as Bugzilla_login
+# and Bugzilla_password with every future call until User.logout is called
+# (which actually just calls _bz_clear_credentials, under GET).
+sub _bz_credentials {
+    my ($self, $user, $pass) = @_;
+    if (@_ == 3) {
+        $self->{_bz_credentials}->{user} = $user;
+        $self->{_bz_credentials}->{pass} = $pass;
+    }
+    return $self->{_bz_credentials};
+}
+sub _bz_clear_credentials { delete $_[0]->{_bz_credentials} }
+
+################################
+# Helpers for RPC test scripts #
+################################
+
+sub bz_log_in {
+    my ($self, $user) = @_;
+    my $username = $self->bz_config->{"${user}_user_login"};
+    my $password = $self->bz_config->{"${user}_user_passwd"};
+
+    if ($self->bz_get_mode) {
+        $self->_bz_credentials($username, $password);
+        return;
+    }
+
+    my $call = $self->bz_call_success(
+        'User.login', { login => $username, password => $password });
+    cmp_ok($call->result->{id}, 'gt', 0, $self->TYPE . ": Logged in as $user");
+    $self->{_bz_credentials}->{token} = $call->result->{token};
+}
+
+sub bz_call_success {
+    my ($self, $method, $orig_args, $test_name) = @_;
+    my $args = $orig_args ? dclone($orig_args) : {};
+
+    if ($self->bz_get_mode and $method eq 'User.logout') {
+        $self->_bz_clear_credentials();
+        return;
+    }
+
+    my $call;
+    # Under XMLRPC::Lite, if we pass undef as the second argument,
+    # it sends a single param <value />, which shows up as an
+    # empty string on the Bugzilla side.
+    if ($self->{_bz_credentials}->{token}) {
+        $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
+    }
+
+    if (scalar keys %$args) {
+        $call = $self->call($method, $args);
+    }
+    else {
+        $call = $self->call($method);
+    }
+    $test_name ||= "$method returned successfully";
+    $self->_handle_undef_response($test_name) if !$call;
+    ok(!$call->fault, $self->TYPE . ": $test_name")
+        or diag($call->faultstring);
+
+    if ($method eq 'User.logout') {
+        delete $self->{_bz_credentials}->{token};
+    }
+    return $call;
+}
+
+sub bz_call_fail {
+    my ($self, $method, $orig_args, $faultstring, $test_name) = @_;
+    my $args = $orig_args ? dclone($orig_args) : {};
+
+    if ($self->{_bz_credentials}->{token}) {
+        $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
+    }
+
+    $test_name ||= "$method failed (as intended)";
+    my $call = $self->call($method, $args);
+    $self->_handle_undef_response($test_name) if !$call;
+    ok($call->fault, $self->TYPE . ": $test_name")
+        or diag("Returned: " . Dumper($call->result));
+    if (defined $faultstring) {
+        cmp_ok(trim($call->faultstring), '=~', $faultstring,
+               $self->TYPE . ": Got correct fault for $method");
+    }
+    ok($call->faultcode
+       && (($call->faultcode < 32000 && $call->faultcode > -32000)
+           # Fault codes 32610 and above are OK because they are errors
+           # that we expect and test for sometimes.
+           || $call->faultcode >= 32610),
+       $self->TYPE . ': Fault code is set properly')
+        or diag("Code: " . $call->faultcode
+                . " Message: " . $call->faultstring);
+
+    return $call;
+}
+
+sub _handle_undef_response {
+    my ($self, $test_name) = @_;
+    my $response = $self->transport->http_response;
+    die "$test_name:\n", $response->as_string;
+}
+
+sub bz_get_products {
+    my ($self) = @_;
+    $self->bz_log_in('QA_Selenium_TEST');
+
+    my $accessible = $self->bz_call_success('Product.get_accessible_products');
+    my $prod_call = $self->bz_call_success('Product.get', $accessible->result);
+    my %products;
+    foreach my $prod (@{ $prod_call->result->{products} }) {
+        $products{$prod->{name}} = $prod->{id};
+    }
+
+    $self->bz_call_success('User.logout');
+    return \%products;
+}
+
+sub _string_array { map { random_string() } (1..$_[0]) }
+
+sub bz_create_test_bugs {
+    my ($self, $second_private) = @_;
+    my $config = $self->bz_config;
+
+    my @whiteboard_strings = _string_array(3);
+    my @summary_strings = _string_array(3);
+
+    my $public_bug = create_bug_fields($config);
+    $public_bug->{whiteboard} = join(' ', @whiteboard_strings);
+    $public_bug->{summary} = join(' ', @summary_strings);
+
+    my $private_bug = dclone($public_bug);
+    if ($second_private) {
+        $private_bug->{product}   = 'QA-Selenium-TEST';
+        $private_bug->{component} = 'QA-Selenium-TEST';
+        $private_bug->{target_milestone} = 'QAMilestone';
+        $private_bug->{version} = 'QAVersion';
+        # Although we don't directly use this, this helps some tests that
+        # depend on the values in $private_bug.
+        $private_bug->{creator} = $config->{PRIVATE_BUG_USER . '_user_login'};
+    }
+
+    my @create_bugs = (
+        { user => 'editbugs',
+          args => $public_bug,
+          test => 'Create a public bug' },
+        { user => $second_private ? PRIVATE_BUG_USER : 'editbugs',
+          args => $private_bug,
+          test => $second_private ? 'Create a private bug'
+                                  : 'Create a second public bug' },
+    );
+
+    my $post_success = sub {
+        my ($call, $t) = @_;
+        my $id = $call->result->{id};
+        $t->{args}->{id} = $id;
+    };
+
+    # Creating the bugs isn't really a test, it's just preliminary work
+    # for the tests. So we just run it with one of the RPC clients.
+    $self->bz_run_tests(tests => \@create_bugs, method => 'Bug.create',
+                        post_success => $post_success);
+
+    return ($public_bug, $private_bug);
+}
+
+sub bz_run_tests {
+    my ($self, %params) = @_;
+    # Required params
+    my $config = $self->bz_config;
+    my $tests  = $params{tests};
+    my $method = $params{method};
+
+    # Optional params
+    my $post_success = $params{post_success};
+    my $pre_call = $params{pre_call};
+
+    my $former_user = '';
+    foreach my $t (@$tests) {
+        # Only logout/login if the user has changed since the last test
+        # (this saves us LOTS of needless logins).
+        my $user = $t->{user} || '';
+        if ($former_user ne $user) {
+            $self->bz_call_success('User.logout') if $former_user;
+            $self->bz_log_in($user) if $user;
+            $former_user = $user;
+        }
+
+        $pre_call->($t, $self) if $pre_call;
+
+        if ($t->{error}) {
+            $self->bz_call_fail($method, $t->{args}, $t->{error}, $t->{test});
+        }
+        else {
+            my $call = $self->bz_call_success($method, $t->{args}, $t->{test});
+            if ($call->result && $post_success) {
+                $post_success->($call, $t, $self);
+            }
+        }
+    }
+
+    $self->bz_call_success('User.logout') if $former_user;
+}
+
+sub bz_test_bug {
+    my ($self, $fields, $bug, $expect, $t, $creation_time) = @_;
+
+    foreach my $field (sort @$fields) {
+        # "description" is used by Bug.create but comments are not returned
+        # by Bug.get or Bug.search.
+        next if $field eq 'description';
+
+        my @include = @{ $t->{args}->{include_fields} || [] };
+        my @exclude = @{ $t->{args}->{exclude_fields} || [] };
+        if ( (@include and !grep($_ eq $field, @include))
+             or (@exclude and grep($_ eq $field, @exclude)) )
+        {
+            ok(!exists $bug->{$field}, "$field is not included")
+              or diag Dumper($bug);
+            next;
+        }
+
+        if ($field =~ /^is_/) {
+            ok(defined $bug->{$field}, $self->TYPE . ": $field is not null");
+            is($bug->{$field} ? 1 : 0, $expect->{$field} ? 1 : 0,
+               $self->TYPE . ": $field has the right boolean value");
+        }
+        elsif ($field eq 'cc') {
+            foreach my $cc_item (@{ $expect->{cc} || [] }) {
+                ok(grep($_ eq $cc_item, @{ $bug->{cc} }),
+                   $self->TYPE . ": $field contains $cc_item");
+            }
+        }
+        elsif ($field eq 'creation_time' or $field eq 'last_change_time') {
+            my $creation_day;
+            # XML-RPC and JSON-RPC have different date formats.
+            if ($self->isa('QA::RPC::XMLRPC')) {
+                $creation_day = $creation_time->ymd('');
+            }
+            else {
+                $creation_day = $creation_time->ymd;
+            }
+
+            like($bug->{$field}, qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
+                 $self->TYPE . ": $field has the right format");
+        }
+        else {
+            is_deeply($bug->{$field}, $expect->{$field},
+                      $self->TYPE . ": $field value is correct");
+        }
+    }
+}
+
+1;
+
+__END__
diff --git a/xt/lib/QA/RPC/JSONRPC.pm b/xt/lib/QA/RPC/JSONRPC.pm
new file mode 100644 (file)
index 0000000..7a085e0
--- /dev/null
@@ -0,0 +1,174 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::RPC::JSONRPC;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../../lib", "$RealBin/../../../../local/lib/perl5";
+
+use QA::RPC;
+BEGIN {
+    our @ISA = qw(QA::RPC);
+
+    if (eval { require JSON::RPC::Client }) {
+        push(@ISA, 'JSON::RPC::Client');
+    }
+    else {
+        require JSON::RPC::Legacy::Client;
+        push(@ISA, 'JSON::RPC::Legacy::Client');
+    }
+}
+
+use URI::Escape;
+
+use constant DATETIME_REGEX => qr/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/;
+sub TYPE {
+    my ($self) = @_;
+    return $self->bz_get_mode ? 'JSON-RPC GET' : 'JSON-RPC';
+}
+
+#################################
+# Consistency with XMLRPC::Lite #
+#################################
+
+sub ua {
+    my $self = shift;
+    if ($self->{ua} and not $self->{ua}->isa('QA::RPC::UserAgent')) {
+        bless $self->{ua}, 'QA::RPC::UserAgent';
+    }
+    return $self->SUPER::ua(@_);
+}
+sub transport { $_[0]->ua }
+
+sub bz_get_mode {
+    my ($self, $value) = @_;
+    $self->{bz_get_mode} = $value if @_ > 1;
+    return $self->{bz_get_mode};
+}
+
+sub _bz_callback {
+    my ($self, $value) = @_;
+    $self->{bz_callback} = $value if @_ > 1;
+    return $self->{bz_callback};
+}
+
+sub call {
+    my $self = shift;
+    my ($method, $args) = @_;
+    my %params = ( method => $method );
+    $params{params} = $args ? [$args] : [];
+
+    my $config = $self->bz_config;
+    my $url = $config->{browser_url} . "/"
+              . $config->{bugzilla_installation} . "/jsonrpc.cgi";
+    my $result;
+    if ($self->bz_get_mode) {
+        my $method_escaped = uri_escape($method);
+        $url .= "?method=$method_escaped";
+        if (my $cred = $self->_bz_credentials) {
+            $args->{Bugzilla_login} = $cred->{user}
+                if !exists $args->{Bugzilla_login};
+            $args->{Bugzilla_password} = $cred->{pass}
+                if !exists $args->{Bugzilla_password};
+        }
+        if ($args) {
+            my $params_json = $self->json->encode($args);
+            my $params_escaped = uri_escape($params_json);
+            $url .= "&params=$params_escaped";
+        }
+        if ($self->version eq '1.1') {
+            $url .= "&version=1.1";
+        }
+        my $callback = delete $args->{callback};
+        if (defined $callback) {
+            $self->_bz_callback($callback);
+            $url .= "&callback=" . uri_escape($callback);
+        }
+        $result = $self->SUPER::call($url);
+    }
+    else {
+        $result = $self->SUPER::call($url, \%params);
+    }
+
+    if ($result) {
+        bless $result, 'QA::RPC::JSONRPC::ReturnObject';
+    }
+    return $result;
+}
+
+sub _get {
+    my $self = shift;
+    my $result = $self->SUPER::_get(@_);
+    # Simple JSONP support for tests. We just remove the callback from
+    # the return value.
+    my $callback = $self->_bz_callback;
+    if (defined $callback and $result->is_success) {
+        my $content = $result->content;
+        $content =~ s/^(?:\/\*\*\/)?\Q$callback(\E(.*)\)$/$1/s;
+        $result->content($content);
+        # We don't need this anymore, and we don't want it to affect
+        # future calls.
+        delete $self->{bz_callback};
+    }
+    return $result;
+}
+
+1;
+
+package QA::RPC::JSONRPC::ReturnObject;
+use strict;
+
+BEGIN {
+    if (eval { require JSON::RPC::Client }) {
+        our @ISA = qw(JSON::RPC::ReturnObject);
+    }
+    else {
+        require JSON::RPC::Legacy::Client;
+        our @ISA = qw(JSON::RPC::Legacy::ReturnObject);
+    }
+}
+
+#################################
+# Consistency with XMLRPC::Lite #
+#################################
+
+sub faultstring { $_[0]->{content}->{error}->{message} }
+sub faultcode   { $_[0]->{content}->{error}->{code}    }
+sub fault { $_[0]->is_error }
+
+1;
+
+package QA::RPC::UserAgent;
+use strict;
+use base qw(LWP::UserAgent);
+
+########################################
+# Consistency with XMLRPC::Lite's ->ua #
+########################################
+
+sub send_request {
+    my $self = shift;
+    my $response = $self->SUPER::send_request(@_);
+    $self->http_response($response);
+    # JSON::RPC::Client can't handle 500 responses, even though
+    # they're required by the JSON-RPC spec.
+    $response->code(200);
+    return $response;
+}
+
+# Copied directly from SOAP::Lite::Transport::HTTP.
+sub http_response {
+    my $self = shift;
+    if (@_) { $self->{'_http_response'} = shift; return $self }
+    return $self->{'_http_response'};
+}
diff --git a/xt/lib/QA/RPC/XMLRPC.pm b/xt/lib/QA/RPC/XMLRPC.pm
new file mode 100644 (file)
index 0000000..cb227fa
--- /dev/null
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::RPC::XMLRPC;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../../lib", "$RealBin/../../../../local/lib/perl5";
+
+use base qw(QA::RPC XMLRPC::Lite);
+
+use constant TYPE => 'XML-RPC';
+use constant DATETIME_REGEX => qr/^\d{8}T\d\d:\d\d:\d\d$/;
+
+1;
+
+__END__
diff --git a/xt/lib/QA/Tests.pm b/xt/lib/QA/Tests.pm
new file mode 100644 (file)
index 0000000..fe5f2d0
--- /dev/null
@@ -0,0 +1,115 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::Tests;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use base qw(Exporter);
+our @EXPORT_OK = qw(
+    PRIVATE_BUG_USER
+    STANDARD_BUG_TESTS
+    bug_tests
+    create_bug_fields
+);
+
+use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ALIAS => 'aaaaaaa12345';
+use constant PRIVATE_BUG_USER => 'QA_Selenium_TEST';
+
+use constant CREATE_BUG => {
+    'priority'     => 'Highest',
+    'status'       => 'CONFIRMED',
+    'version'      => 'unspecified',
+    'creator'      => 'editbugs',
+    'description'  => '-- Comment Created By Bugzilla XML-RPC Tests --',
+    'cc'           => ['unprivileged'],
+    'component'    => 'c1',
+    'platform'     => 'PC',
+    # It's necessary to assign the bug to somebody who isn't in the
+    # timetracking group, for the Bug.update tests.
+    'assigned_to'  => PRIVATE_BUG_USER,
+    'summary'      => 'WebService Test Bug',
+    'product'      => 'Another Product',
+    'op_sys'       => 'Linux',
+    'severity'     => 'normal',
+    'qa_contact'   => 'canconfirm',
+     version       => 'Another1',
+     url           => 'http://www.bugzilla.org/',
+     target_milestone => 'AnotherMS1',
+};
+
+sub create_bug_fields {
+    my ($config) = @_;
+    my %bug = %{ CREATE_BUG() };
+    foreach my $field (qw(creator assigned_to qa_contact)) {
+        my $value = $bug{$field};
+        $bug{$field} = $config->{"${value}_user_login"};
+    }
+    $bug{cc} = [map { $config->{$_ . "_user_login"} } @{ $bug{cc} }];
+    return \%bug;
+}
+
+sub bug_tests {
+    my ($public_id, $private_id) = @_;
+    return [
+    { args  => { ids => [$private_id] },
+      error => "You are not authorized to access",
+      test  => 'Logged-out user cannot access a private bug',
+    },
+    { args => { ids => [$public_id] },
+      test => 'Logged-out user can access a public bug.',
+    },
+    { args  => { ids => [INVALID_BUG_ID] },
+      error => "not a valid bug number",
+      test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
+    },
+    { args  => { ids => [undef] },
+      error => "You must enter a valid bug number",
+      test  => 'Passing undef as bug id param returns error "Invalid Bug ID"',
+    },
+    { args  => { ids => [INVALID_BUG_ALIAS] },
+      error => "nor an alias to a bug",
+      test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+    },
+
+    { user  => 'editbugs',
+      args  => { ids => [$private_id] },
+      error => "You are not authorized to access",
+      test  => 'Access to a private bug is denied to a user without privs',
+    },
+    { user => 'unprivileged',
+      args => { ids => [$public_id] },
+      test => 'User without privs can access a public bug',
+    },
+    { user => 'admin',
+      args => { ids => [$public_id] },
+      test => 'Admin can access a public bug.',
+    },
+    { user => PRIVATE_BUG_USER,
+      args => { ids => [$private_id] },
+      test => 'User with privs can successfully access a private bug',
+    },
+    # This helps webservice_bug_attachment get private attachment ids
+    # from the public bug, and doesn't hurt for the other tests.
+    { user => PRIVATE_BUG_USER,
+      args => { ids => [$public_id] },
+      test => 'User with privs can also access the public bug',
+    },
+    ];
+}
+
+use constant STANDARD_BUG_TESTS => bug_tests('public_bug', 'private_bug');
+
+1;
diff --git a/xt/lib/QA/Util.pm b/xt/lib/QA/Util.pm
new file mode 100644 (file)
index 0000000..e122e41
--- /dev/null
@@ -0,0 +1,372 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::Util;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use autodie;
+use Data::Dumper;
+use Test::More;
+
+use parent qw(Exporter);
+@QA::Util::EXPORT = qw(
+    trim
+    url_quote
+    random_string
+
+    log_in
+    logout
+    file_bug_in_product
+    create_bug
+    edit_bug
+    edit_bug_and_return
+    go_to_bug
+    go_to_home
+    go_to_admin
+    edit_product
+    add_product
+    open_advanced_search_page
+    set_parameters
+
+    get_selenium
+    get_rpc_clients
+    get_config
+
+    WAIT_TIME
+    CHROME_MODE
+);
+
+# How long we wait for pages to load.
+use constant WAIT_TIME => 60000;
+use constant CONF_FILE =>  "$RealBin/../config/selenium_test.conf";
+use constant CHROME_MODE => 1;
+use constant NDASH => chr(0x2013);
+
+#####################
+# Utility Functions #
+#####################
+
+sub random_string {
+    my $size = shift || 30; # default to 30 chars if nothing specified
+    return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size));
+}
+
+# Remove consecutive as well as leading and trailing whitespaces.
+sub trim {
+    my ($str) = @_;
+    if ($str) {
+      $str =~ s/[\r\n\t\s]+/ /g;
+      $str =~ s/^\s+//g;
+      $str =~ s/\s+$//g;
+    }
+    return $str;
+}
+
+# This originally came from CGI.pm, by Lincoln D. Stein
+sub url_quote {
+    my ($toencode) = (@_);
+    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
+    return $toencode;
+}
+
+###################
+# Setup Functions #
+###################
+
+sub get_config {
+    # read the test configuration file
+    my $conf_file = CONF_FILE;
+    my $config = do($conf_file)
+        or die "can't read configuration '$conf_file': $!$@";
+    return $config;
+}
+
+sub get_selenium {
+    my $chrome_mode = shift;
+    my $config = get_config();
+
+    require Test::WWW::Selenium;
+    require WWW::Selenium::Util;
+
+    if (!WWW::Selenium::Util::server_is_running()) {
+        die "Selenium Server isn't running!";
+    }
+
+    my $sel = Test::WWW::Selenium->new(
+        host        => $config->{host},
+        port        => $config->{port},
+        browser     => $chrome_mode ? $config->{experimental_browser_launcher} : $config->{browser},
+        browser_url => $config->{browser_url}
+    );
+
+    return ($sel, $config);
+}
+
+sub get_xmlrpc_client {
+    my $config = get_config();
+    my $xmlrpc_url = $config->{browser_url} . "/" .
+                     $config->{bugzilla_installation} . "/xmlrpc.cgi";
+
+    require QA::RPC::XMLRPC;
+    my $rpc = QA::RPC::XMLRPC->new(proxy => $xmlrpc_url);
+    return ($rpc, $config);
+}
+
+sub get_jsonrpc_client {
+    my ($get_mode) = @_;
+    require QA::RPC::JSONRPC;
+    my $rpc = QA::RPC::JSONRPC->new();
+    # If we don't set a long timeout, then the Bug.add_comment test
+    # where we add a too-large comment fails.
+    $rpc->transport->timeout(180);
+    $rpc->version($get_mode ? '1.1' : '1.0');
+    $rpc->bz_get_mode($get_mode);
+    return $rpc;
+}
+
+sub get_rpc_clients {
+    my ($xmlrpc, $config) = get_xmlrpc_client();
+    my $jsonrpc = get_jsonrpc_client();
+    my $jsonrpc_get = get_jsonrpc_client('GET');
+    return ($config, $xmlrpc, $jsonrpc, $jsonrpc_get);
+}
+
+################################
+# Helpers for Selenium Scripts #
+################################
+
+sub go_to_home {
+    my ($sel, $config) = @_;
+    $sel->open_ok("/$config->{bugzilla_installation}/", undef, "Go to the home page");
+    $sel->title_is("Bugzilla Main Page");
+}
+
+# Go to the home/login page and log in.
+sub log_in {
+    my ($sel, $config, $user) = @_;
+
+    go_to_home($sel, $config);
+    $sel->type_ok("Bugzilla_login_top", $config->{"${user}_user_login"}, "Enter $user login name");
+    $sel->type_ok("Bugzilla_password_top", $config->{"${user}_user_passwd"}, "Enter $user password");
+    $sel->click_ok("log_in_top", undef, "Submit credentials");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Bugzilla Main Page", "User is logged in");
+}
+
+# Log out. Will fail if you are not logged in.
+sub logout {
+    my $sel = shift;
+
+    $sel->click_ok("link=Log out", undef, "Logout");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Logged Out");
+}
+
+# Display the bug form to enter a bug in the given product.
+sub file_bug_in_product {
+    my ($sel, $product, $classification) = @_;
+
+    $classification ||= "Unclassified";
+    $sel->click_ok("link=New", undef, "Go create a new bug");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    my $title = $sel->get_title();
+    if ($sel->is_text_present("Select Classification")) {
+        ok(1, "More than one enterable classification available. Display them in a list");
+        $sel->click_ok("link=$classification", undef, "Choose $classification");
+        $sel->wait_for_page_to_load(WAIT_TIME);
+        $title = $sel->get_title();
+    }
+    if ($title eq "Enter Bug") {
+        ok(1, "Display the list of enterable products");
+        $sel->click_ok("link=$product", undef, "Choose $product");
+        $sel->wait_for_page_to_load(WAIT_TIME);
+    }
+    else {
+        ok(1, "Only one product available in $classification. Skipping the 'Choose product' page.")
+    }
+    $sel->title_is("Enter Bug: $product", "Display form to enter bug data");
+}
+
+sub create_bug {
+    my ($sel, $bug_summary) = @_;
+    my $ndash = NDASH;
+
+    $sel->click_ok('commit');
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    my $bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+    $sel->title_like(qr/$bug_id $ndash( \(.*\))? $bug_summary/, "Bug $bug_id created with summary '$bug_summary'");
+    return $bug_id;
+}
+
+sub edit_bug {
+    my ($sel, $bug_id, $bug_summary, $options) = @_;
+    my $ndash = NDASH;
+    my $btn_id = $options ? $options->{id} : 'commit';
+
+    $sel->click_ok($btn_id);
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("$bug_id $ndash $bug_summary", "Changes submitted to bug $bug_id");
+    # If the web browser doesn't support history.ReplaceState or has it turned off,
+    # "Bug FIXME processed" is displayed instead (as in Bugzilla 4.0 and older).
+    # $sel->title_is("Bug $bug_id processed", "Changes submitted to bug $bug_id");
+}
+
+sub edit_bug_and_return {
+    my ($sel, $bug_id, $bug_summary, $options) = @_;
+    my $ndash = NDASH;
+    edit_bug($sel, $bug_id, $bug_summary, $options);
+    $sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug_id')]");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("$bug_id $ndash $bug_summary", "Returning back to bug $bug_id");
+}
+
+# Go to show_bug.cgi.
+sub go_to_bug {
+    my ($sel, $bug_id) = @_;
+
+    $sel->type_ok("quicksearch_top", $bug_id);
+    $sel->click_ok("find_top", undef, "Go to bug $bug_id");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    my $bug_title = $sel->get_title();
+    utf8::encode($bug_title) if utf8::is_utf8($bug_title);
+    $sel->title_like(qr/^$bug_id /, $bug_title);
+}
+
+# Go to admin.cgi.
+sub go_to_admin {
+    my $sel = shift;
+
+    $sel->click_ok("link=Administration", undef, "Go to the Admin page");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_like(qr/^Administer your installation/, "Display admin.cgi");
+}
+
+# Go to editproducts.cgi and display the given product.
+sub edit_product {
+    my ($sel, $product, $classification) = @_;
+
+    $classification ||= "Unclassified";
+    go_to_admin($sel);
+    $sel->click_ok("link=Products", undef, "Go to the Products page");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    my $title = $sel->get_title();
+    if ($title eq "Select Classification") {
+        ok(1, "More than one enterable classification available. Display them in a list");
+        $sel->click_ok("link=$classification", undef, "Choose $classification");
+        $sel->wait_for_page_to_load(WAIT_TIME);
+    }
+    else {
+        $sel->title_is("Select product", "Display the list of enterable products");
+    }
+    $sel->click_ok("link=$product", undef, "Choose $product");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Edit Product '$product'", "Display properties of $product");
+}
+
+sub add_product {
+    my ($sel, $classification) = @_;
+
+    $classification ||= "Unclassified";
+    go_to_admin($sel);
+    $sel->click_ok("link=Products", undef, "Go to the Products page");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    my $title = $sel->get_title();
+    if ($title eq "Select Classification") {
+        ok(1, "More than one enterable classification available. Display them in a list");
+        $sel->click_ok("//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]",
+                       undef, "Add product to $classification");
+    }
+    else {
+        $sel->title_is("Select product", "Display the list of enterable products");
+        $sel->click_ok("link=Add", undef, "Add a new product");
+    }
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Add Product", "Display the new product form");
+}
+
+sub open_advanced_search_page {
+    my $sel = shift;
+
+    $sel->click_ok("link=Search");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    my $title = $sel->get_title();
+    if ($title eq "Simple Search") {
+        ok(1, "Display the simple search form");
+        $sel->click_ok("link=Advanced Search");
+        $sel->wait_for_page_to_load(WAIT_TIME);
+    }
+    $sel->title_is("Search for bugs", "Display the Advanced search form");
+}
+
+# $params is a hashref of the form:
+# {section1 => { param1 => {type => '(text|select)', value => 'foo'},
+#                param2 => {type => '(text|select)', value => 'bar'},
+#                param3 => undef },
+#  section2 => { param4 => ...},
+# }
+# section1, section2, ... is the name of the section
+# param1, param2, ... is the name of the parameter (which must belong to the given section)
+# type => 'text' is for text fields
+# type => 'select' is for drop-down select fields
+# undef is for radio buttons (in which case the parameter must be the ID of the radio button)
+# value => 'foo' is the value of the parameter (either text or label)
+sub set_parameters {
+    my ($sel, $params) = @_;
+
+    go_to_admin($sel);
+    $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Configuration: Required Settings");
+    my $last_section = "Required Settings";
+
+    foreach my $section (keys %$params) {
+        if ($section ne $last_section) {
+            $sel->click_ok("link=$section");
+            $sel->wait_for_page_to_load_ok(WAIT_TIME);
+            $sel->title_is("Configuration: $section");
+            $last_section = $section;
+        }
+        my $param_list = $params->{$section};
+        foreach my $param (keys %$param_list) {
+            my $data = $param_list->{$param};
+            if (defined $data) {
+                my $type = $data->{type};
+                my $value = $data->{value};
+
+                if ($type eq 'text') {
+                    $sel->type_ok($param, $value);
+                }
+                elsif ($type eq 'select') {
+                    $sel->select_ok($param, "label=$value");
+                }
+                else {
+                    ok(0, "Unknown parameter type: $type");
+                }
+            }
+            else {
+                # If the value is undefined, then the param name is
+                # the ID of the radio button.
+                $sel->click_ok($param);
+            }
+        }
+        $sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes");
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_is("Parameters Updated");
+    }
+}
+
+1;
+
+__END__
diff --git a/xt/rest/bugzilla.t b/xt/rest/bugzilla.t
new file mode 100644 (file)
index 0000000..a176d1c
--- /dev/null
@@ -0,0 +1,60 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#######################################
+# Tests for REST calls in Bugzilla.pm #
+#######################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 11;
+use QA::REST;
+
+my $rest = get_rest_client();
+my $config = $rest->bz_config;
+
+my $version = $rest->call('version')->{version};
+ok($version, "GET /rest/version returns $version");
+
+my $extensions = $rest->call('extensions')->{extensions};
+isa_ok($extensions, 'HASH', 'GET /rest/extensions');
+my @ext_names = sort keys %$extensions;
+# There is always at least the QA extension enabled.
+ok(scalar(@ext_names), scalar(@ext_names) . ' extension(s) found: ' . join(', ', @ext_names));
+ok($extensions->{QA}, 'The QA extension is enabled, with version ' . $extensions->{QA}->{version});
+
+my $timezone = $rest->call('timezone')->{timezone};
+ok($timezone, "GET /rest/timezone retuns $timezone");
+
+my $time = $rest->call('time');
+foreach my $type (qw(db_time web_time)) {
+    ok($time->{$type}, "GET /rest/time returns $type = " . $time->{$type});
+}
+
+# Logged-out users can only access the maintainer and requirelogin parameters.
+my $params = $rest->call('parameters')->{parameters};
+my @param_names = sort keys %$params;
+ok(@param_names == 2 && defined $params->{maintainer} && defined $params->{requirelogin},
+   'Only 2 parameters accessible to logged-out users: ' . join(', ', @param_names));
+
+# Powerless users can access much more parameters.
+$params = $rest->call('parameters', { api_key => $config->{unprivileged_user_api_key} })->{parameters};
+@param_names = sort keys %$params;
+ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to powerless users');
+
+# Admins can access all parameters.
+$params = $rest->call('parameters', { api_key => $config->{admin_user_api_key} })->{parameters};
+@param_names = sort keys %$params;
+ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to admins');
+
+my $timestamp = $rest->call('last_audit_time')->{last_audit_time};
+ok($timestamp, "GET /rest/last_audit_time returns $timestamp");
diff --git a/xt/rest/classification.t b/xt/rest/classification.t
new file mode 100644 (file)
index 0000000..d006de9
--- /dev/null
@@ -0,0 +1,61 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#############################################
+# Tests for REST calls in Classification.pm #
+#############################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 7;
+use QA::REST;
+
+my $rest = get_rest_client();
+my $config = $rest->bz_config;
+my $args = { api_key => $config->{admin_user_api_key} };
+
+my $params = $rest->call('parameters', $args)->{parameters};
+my $use_class = $params->{useclassification};
+ok(defined($use_class), 'Classifications are ' . ($use_class ? 'enabled' : 'disabled'));
+
+# Admins can always access classifications, even when they are disabled.
+my $class = $rest->call('classification/1', $args)->{classifications}->[0];
+ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'");
+my @products = sort map { $_->{name} } @{ $class->{products} };
+ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products));
+
+$class = $rest->call('classification/Class2_QA', $args)->{classifications}->[0];
+ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'");
+@products = sort map { $_->{name} } @{ $class->{products} };
+ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products));
+
+if ($use_class) {
+    # When classifications are enabled, everybody can query classifications...
+    # ... including logged-out users.
+    $class = $rest->call('classification/1')->{classifications}->[0];
+    ok($class->{id}, 'Logged-out users can access classification ' . $class->{name});
+    # ... and non-admins.
+    $class = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} })->{classifications}->[0];
+    ok($class->{id}, 'Non-admins can access classification ' . $class->{name});
+}
+else {
+    # When classifications are disabled, only users in the 'editclassifications'
+    # group can access this method...
+    # ... logged-out users get an error.
+    my $error = $rest->call('classification/1', undef, undef, MUST_FAIL);
+    ok($error->{error} && $error->{code} == 900,
+       'Logged-out users cannot query classifications when disabled: ' . $error->{message});
+    # ... as well as non-admins.
+    $error = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} }, undef, MUST_FAIL);
+    ok($error->{error} && $error->{code} == 900,
+       'Non-admins cannot query classifications when disabled: ' . $error->{message});
+}
diff --git a/xt/search.t b/xt/search.t
deleted file mode 100644 (file)
index 8f6e2e9..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/perl -w
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-# For a description of this test, see Bugzilla::Test::Search
-# in xt/lib/.
-
-use strict;
-use warnings;
-use lib qw(. xt/lib lib local/lib/perl5);
-use Bugzilla;
-use Bugzilla::Constants;
-use Bugzilla::Test::Search;
-use Getopt::Long;
-use Pod::Usage;
-
-use Test::More;
-
-my %switches;
-GetOptions(\%switches, 'operators=s', 'top-operators=s', 'long',
-                       'add-custom-fields', 'help|h') || die $@;
-
-pod2usage(verbose => 1) if $switches{'help'};
-
-plan skip_all => "BZ_WRITE_TESTS environment variable not set"
-  if !$ENV{BZ_WRITE_TESTS};
-
-Bugzilla->usage_mode(USAGE_MODE_TEST);
-
-my $test = new Bugzilla::Test::Search(\%switches);
-plan tests => $test->num_tests;
-$test->run();
-
-__END__
-
-=head1 NAME
-
-search.t - Test L<Bugzilla::Search>
-
-=head1 DESCRIPTION
-
-This test tests L<Bugzilla::Search>.
-
-Note that users may be prevented from writing new bugs, products, components,
-etc. to your database while this test is running.
-
-=head1 OPTIONS
-
-=over
-
-=item --long
-
-Run AND and OR tests in addition to normal tests. Specifying
---long without also specifying L</--top-operators> is likely to
-run your system out of memory.
-
-=item --add-custom-fields
-
-This adds every type of custom field to the database, so that they can
-all be tested. Note that this B<CANNOT BE REVERSED>, so do not use this
-switch on a production installation.
-
-=item --operators=a,b,c
-
-Limit the test to testing only the listed operators.
-
-=item --top-operators=a,b,c
-
-Limit the top-level tested operators to the following list. This
-means that for normal tests, only the listed operators will be tested.
-However, for OR and AND tests, all other operators will be tested
-along with the operators you listed.
-
-=item --help
-
-Display this help.
-
-=back
diff --git a/xt/selenium/bug_edit.t b/xt/selenium/bug_edit.t
new file mode 100644 (file)
index 0000000..46e7e6c
--- /dev/null
@@ -0,0 +1,441 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields" => {"usestatuswhiteboard-on" => undef} });
+
+# Clear the saved search, in case this test didn't complete previously.
+if ($sel->is_text_present("My bugs from QA_Selenium")) {
+    $sel->click_ok("link=My bugs from QA_Selenium");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Bug List: My bugs from QA_Selenium");
+    $sel->click_ok("forget_search");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Search is gone");
+    $sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone");
+}
+
+# Just in case the test failed before completion previously, reset the CANEDIT bit.
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Master");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Group: Master");
+my $group_url = $sel->get_location();
+$group_url =~ /group=(\d+)$/;
+my $master_gid = $1;
+
+clear_canedit_on_testproduct($sel, $master_gid);
+logout($sel);
+
+# First create a bug.
+
+log_in($sel, $config, 'QA_Selenium_TEST');
+file_bug_in_product($sel, 'TestProduct');
+my $bug_summary = "Test bug editing";
+$sel->select_ok("bug_severity", "label=critical");
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "ploc");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# Now edit field values of the bug you just filed.
+
+$sel->select_ok("rep_platform", "label=Other");
+$sel->select_ok("op_sys", "label=Other");
+$sel->select_ok("priority", "label=Highest");
+$sel->select_ok("bug_severity", "label=blocker");
+$sel->type_ok("bug_file_loc", "foo.cgi?action=bar");
+$sel->type_ok("status_whiteboard", "[Selenium was here]");
+$sel->type_ok("comment", "new comment from me :)");
+$sel->select_ok("bug_status", "label=RESOLVED");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# Now move the bug into another product, which has a mandatory group.
+
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->select_ok("product", "label=QA-Selenium-TEST");
+$sel->type_ok("comment", "moving to QA-Selenium-TEST");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Verify New Product Details...");
+$sel->select_ok("component", "label=QA-Selenium-TEST");
+$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
+ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable");
+$sel->is_checked_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
+edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "change_product"});
+$sel->select_ok("bug_severity", "label=normal");
+$sel->select_ok("priority", "label=High");
+$sel->select_ok("rep_platform", "label=All");
+$sel->select_ok("op_sys", "label=All");
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", $config->{admin_user_login});
+$sel->type_ok("comment", "Unchecking the reporter_accessible checkbox");
+# This checkbox is checked by default.
+$sel->click_ok("reporter_accessible");
+$sel->select_ok("bug_status", "label=VERIFIED");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->type_ok("comment", "I am the reporter, but I can see the bug anyway as I belong to the mandatory group");
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
+
+# The admin is not in the mandatory group, but he has been CC'ed,
+# so he can view and edit the bug (as he has editbugs privs by inheritance).
+
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug1_id);
+$sel->select_ok("bug_severity", "label=blocker");
+$sel->select_ok("priority", "label=Highest");
+$sel->type_ok("status_whiteboard", "[Selenium was here][admin too]");
+$sel->select_ok("bug_status", "label=CONFIRMED");
+$sel->click_ok("bz_assignee_edit_action");
+$sel->type_ok("assigned_to", $config->{admin_user_login});
+$sel->type_ok("comment", "I have editbugs privs. Taking!");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", $config->{unprivileged_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
+
+# The powerless user can see the restricted bug, as he has been CC'ed.
+
+log_in($sel, $config, 'unprivileged');
+go_to_bug($sel, $bug1_id);
+$sel->is_text_present_ok("I have editbugs privs. Taking!");
+logout($sel);
+
+# Now turn off cclist_accessible, which will prevent
+# the powerless user to see the bug again.
+
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("cclist_accessible");
+$sel->type_ok("comment", "I am allowed to turn off cclist_accessible despite not being in the mandatory group");
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
+
+# The powerless user cannot see the restricted bug anymore.
+
+log_in($sel, $config, 'unprivileged');
+$sel->type_ok("quicksearch_top", $bug1_id);
+$sel->click_ok("find_top");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug Access Denied");
+$sel->is_text_present_ok("You are not authorized to access bug #$bug1_id");
+logout($sel);
+
+# Move the bug back to TestProduct, which has no group restrictions.
+
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug1_id);
+$sel->select_ok("product", "label=TestProduct");
+# When selecting a new product, Bugzilla tries to reassign the bug by default,
+# so we have to uncheck it.
+$sel->click_ok("set_default_assignee");
+$sel->uncheck_ok("set_default_assignee");
+$sel->type_ok("comment", "-> Moving back to Testproduct.");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Verify New Product Details...");
+$sel->select_ok("component", "label=TestComponent");
+$sel->is_text_present_ok("These groups are not legal for the 'TestProduct' product or you are not allowed to restrict bugs to these groups");
+$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]');
+ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable");
+ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not selected");
+$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]');
+$sel->is_editable_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]');
+ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="Master"]'), "Master group not selected by default");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "change_product"});
+logout($sel);
+
+# The unprivileged user can view the bug again, but cannot
+# edit it, except adding comments.
+
+log_in($sel, $config, 'unprivileged');
+go_to_bug($sel, $bug1_id);
+$sel->type_ok("comment", "I have no privs, I can only comment (and remove people from the CC list)");
+ok(!$sel->is_element_present('//select[@name="product"]'), "Product field not editable");
+ok(!$sel->is_element_present('//select[@name="bug_severity"]'), "Severity field not editable");
+ok(!$sel->is_element_present('//select[@name="priority"]'), "Priority field not editable");
+ok(!$sel->is_element_present('//select[@name="op_sys"]'), "OS field not editable");
+ok(!$sel->is_element_present('//select[@name="rep_platform"]'), "Hardware field not editable");
+$sel->click_ok("cc_edit_area_showhide");
+$sel->add_selection_ok("cc", "label=" . $config->{admin_user_login});
+$sel->click_ok("removecc");
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
+
+# Now let's test the CANEDIT bit.
+
+log_in($sel, $config, 'admin');
+edit_product($sel, "TestProduct");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Group Controls for TestProduct");
+$sel->check_ok("canedit_$master_gid");
+$sel->click_ok("submit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Update group access controls for TestProduct");
+
+# The user is in the master group, so he can comment.
+
+go_to_bug($sel, $bug1_id);
+$sel->type_ok("comment", "Do nothing except adding a comment...");
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
+
+# This user is not in the master group, so he cannot comment.
+
+log_in($sel, $config, 'QA_Selenium_TEST');
+go_to_bug($sel, $bug1_id);
+$sel->type_ok("comment", "Just a comment too...");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Edit Access Denied");
+$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct.");
+logout($sel);
+
+# Test searches and "format for printing".
+
+log_in($sel, $config, 'admin');
+open_advanced_search_page($sel);
+$sel->remove_all_selections_ok("product");
+$sel->add_selection_ok("product", "TestProduct");
+$sel->remove_all_selections_ok("bug_status");
+$sel->remove_all_selections_ok("resolution");
+$sel->check_ok("emailassigned_to1");
+$sel->select_ok("emailtype1", "label=is");
+$sel->type_ok("email1", $config->{admin_user_login});
+$sel->check_ok("emailassigned_to2");
+$sel->check_ok("emailqa_contact2");
+$sel->check_ok("emailcc2");
+$sel->select_ok("emailtype2", "label=is");
+$sel->type_ok("email2", $config->{QA_Selenium_TEST_user_login});
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+
+$sel->is_text_present_ok("One bug found.");
+$sel->type_ok("save_newqueryname", "My bugs from QA_Selenium");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+$sel->is_text_present_ok("OK, you have a new search named My bugs from QA_Selenium.");
+$sel->click_ok("link=My bugs from QA_Selenium");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: My bugs from QA_Selenium");
+$sel->click_ok("long_format");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Full Text Bug Listing");
+$sel->is_text_present_ok("Bug $bug1_id");
+$sel->is_text_present_ok("Status: CONFIRMED");
+$sel->is_text_present_ok("Reporter: QA-Selenium-TEST <$config->{QA_Selenium_TEST_user_login}>");
+$sel->is_text_present_ok("Assignee: admin <$config->{admin_user_login}>");
+$sel->is_text_present_ok("Severity: blocker");
+$sel->is_text_present_ok("Priority: Highest");
+$sel->is_text_present_ok("I have no privs, I can only comment");
+logout($sel);
+
+# Let's create a 2nd bug by this user so that we can test mass-change
+# using the saved search the admin just created.
+
+log_in($sel, $config, 'QA_Selenium_TEST');
+file_bug_in_product($sel, 'TestProduct');
+my $bug_summary2 = "New bug from me";
+$sel->select_ok("bug_severity", "label=blocker");
+$sel->type_ok("short_desc", $bug_summary2);
+# We turned on the CANEDIT bit for TestProduct.
+$sel->type_ok("comment", "I can enter a new bug, but not edit it, right?");
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+# Clicking the "Back" button and resubmitting the form again should trigger a warning.
+
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Enter Bug: TestProduct");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Suspicious Action");
+$sel->is_text_present_ok("no valid token for the create_bug action while processing the 'post_bug.cgi' script");
+$sel->click_ok("confirm");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/\d+ \S $bug_summary2/, "Bug created");
+$sel->type_ok("comment", "New comment not allowed");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Edit Access Denied");
+$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct.");
+logout($sel);
+
+# Reassign the newly created bug to the admin.
+
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug2_id);
+$sel->click_ok("bz_assignee_edit_action");
+$sel->type_ok("assigned_to", $config->{admin_user_login});
+$sel->type_ok("comment", "Taking!");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+# Test mass-change.
+
+$sel->click_ok("link=My bugs from QA_Selenium");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: My bugs from QA_Selenium");
+$sel->is_text_present_ok("2 bugs found");
+$sel->click_ok("mass_change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->click_ok("check_all");
+$sel->type_ok("comment", 'Mass change"');
+$sel->select_ok("bug_status", "label=RESOLVED");
+$sel->select_ok("resolution", "label=WORKSFORME");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugs processed");
+
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/$bug1_id /);
+$sel->selected_label_is("resolution", "WORKSFORME");
+$sel->select_ok("resolution", "label=INVALID");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->selected_label_is("resolution", "INVALID");
+
+$sel->click_ok("link=History");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Changes made to bug $bug1_id");
+$sel->is_text_present_ok("URL foo.cgi?action=bar");
+$sel->is_text_present_ok("Severity critical blocker");
+$sel->is_text_present_ok("Whiteboard [Selenium was here] [Selenium was here][admin too]");
+$sel->is_text_present_ok("Product QA-Selenium-TEST TestProduct");
+$sel->is_text_present_ok("Status CONFIRMED RESOLVED");
+
+# Last step: move bugs to another DB, if the extension is enabled.
+
+if ($config->{test_extensions}) {
+    set_parameters($sel, { "Bug Moving" => {"move-to-url"     => {type => "text", value => 'http://www.foo.com/'},
+                                            "move-to-address" => {type => "text", value => 'import@foo.com'},
+                                            "movers"          => {type => "text", value => $config->{admin_user_login}}
+                                           }
+                         });
+
+    $sel->click_ok("link=My bugs from QA_Selenium");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Bug List: My bugs from QA_Selenium");
+    $sel->is_text_present_ok("2 bugs found");
+    $sel->click_ok("mass_change");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Bug List");
+    $sel->click_ok("check_all");
+    $sel->type_ok("comment", "-> moved");
+    $sel->click_ok('oldbugmove');
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Bugs processed");
+    $sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+    $sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+    $sel->click_ok("link=$bug2_id");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_like(qr/^$bug2_id/);
+    $sel->selected_label_is("resolution", "MOVED");
+    $sel->is_text_present_ok("Bug moved to http://www.foo.com/.");
+
+    # Disable bug moving again.
+    set_parameters($sel, { "Bug Moving" => {"movers" => {type => "text", value => ""}} });
+}
+
+# Make sure token checks are working correctly for single bug editing and mass change,
+# first with no token, then with an invalid token.
+
+foreach my $params (["no_token_single_bug", ""], ["invalid_token_single_bug", "&token=1"]) {
+    my ($comment, $token) = @$params;
+    $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id=$bug1_id&comment=$comment$token",
+                  undef, "Edit a single bug with " . ($token ? "an invalid" : "no") . " token");
+    $sel->title_is("Suspicious Action");
+    $sel->is_text_present_ok($token ? "an invalid token" : "web browser directly");
+    edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "confirm"});
+    $sel->is_text_present_ok($comment);
+}
+
+foreach my $params (["no_token_mass_change", ""], ["invalid_token_mass_change", "&token=1"]) {
+    my ($comment, $token) = @$params;
+    $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id_$bug1_id=1&id_$bug2_id=1&comment=$comment$token",
+                  undef, "Mass change with " . ($token ? "an invalid" : "no") . " token");
+    $sel->title_is("Suspicious Action");
+    $sel->is_text_present_ok("no valid token for the buglist_mass_change action");
+    $sel->click_ok("confirm");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Bugs processed");
+    foreach my $bug_id ($bug1_id, $bug2_id) {
+        $sel->click_ok("link=$bug_id");
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_like(qr/^$bug_id /);
+        $sel->is_text_present_ok($comment);
+        next if $bug_id == $bug2_id;
+        $sel->go_back_ok();
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_is("Bugs processed");
+    }
+}
+
+# Now move these bugs out of our radar.
+
+$sel->click_ok("link=My bugs from QA_Selenium");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: My bugs from QA_Selenium");
+$sel->is_text_present_ok("2 bugs found");
+$sel->click_ok("mass_change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->click_ok("check_all");
+$sel->type_ok("comment", "Reassigning to the reporter");
+$sel->type_ok("assigned_to", $config->{QA_Selenium_TEST_user_login});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugs processed");
+
+# Now delete the saved search.
+
+$sel->click_ok("link=My bugs from QA_Selenium");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: My bugs from QA_Selenium");
+$sel->click_ok("forget_search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search is gone");
+$sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone");
+
+# Reset the CANEDIT bit. We want it to be turned off by default.
+clear_canedit_on_testproduct($sel, $master_gid);
+logout($sel);
+
+sub clear_canedit_on_testproduct {
+    my ($sel, $master_gid) = @_;
+
+    edit_product($sel, "TestProduct");
+    $sel->click_ok("link=Edit Group Access Controls:");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Edit Group Controls for TestProduct");
+    $sel->uncheck_ok("canedit_$master_gid");
+    $sel->click_ok("submit");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Update group access controls for TestProduct");
+}
diff --git a/xt/selenium/choose_priority.t b/xt/selenium/choose_priority.t
new file mode 100644 (file)
index 0000000..1089d20
--- /dev/null
@@ -0,0 +1,30 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-off" => undef} });
+file_bug_in_product($sel, "TestProduct");
+ok(!$sel->is_text_present("Priority"), "The Priority label is not present");
+ok(!$sel->is_element_present("//select[\@name='priority']"), "The Priority drop-down menu is not present");
+set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-on" => undef} });
+file_bug_in_product($sel, "TestProduct");
+$sel->is_text_present_ok("Priority");
+$sel->is_element_present_ok("//select[\@name='priority']");
+logout($sel);
diff --git a/xt/selenium/classifications.t b/xt/selenium/classifications.t
new file mode 100644 (file)
index 0000000..4d5d012
--- /dev/null
@@ -0,0 +1,142 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Enable classifications
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} });
+
+# Create a new classification.
+
+go_to_admin($sel);
+$sel->click_ok("link=Classifications");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select classification");
+
+# Delete old classifications if this script failed.
+# Accessing action=delete directly must 1) trigger the security check page,
+# and 2) automatically reclassify products in this classification.
+if ($sel->is_text_present("cone")) {
+    $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&amp;classification=cone");
+    $sel->title_is("Suspicious Action");
+    $sel->click_ok("confirm");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Classification Deleted");
+}
+if ($sel->is_text_present("ctwo")) {
+    $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&amp;classification=ctwo");
+    $sel->title_is("Suspicious Action");
+    $sel->click_ok("confirm");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Classification Deleted");
+}
+
+$sel->click_ok("link=Add a new classification");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add new classification");
+$sel->type_ok("classification", "cone");
+$sel->type_ok("description", "Classification number 1");
+$sel->click_ok('//input[@type="submit" and @value="Add"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Classification Created");
+
+# Add TestProduct to the new classification. There should be no other
+# products in this classification.
+
+$sel->select_ok("prodlist", "value=TestProduct");
+$sel->click_ok("add_products");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Reclassify products");
+my @products = $sel->get_select_options("myprodlist");
+ok(scalar @products == 1 && $products[0] eq 'TestProduct', "TestProduct successfully added to 'cone'");
+
+# Create a new bug in this product/classification.
+
+file_bug_in_product($sel, 'TestProduct', 'cone');
+my $bug_summary = "Bug in classification cone";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Created by Selenium with classifications turned on");
+create_bug($sel, $bug_summary);
+
+# Rename 'cone' to 'Unclassified', which must be rejected as it already exists,
+# then to 'ctwo', which is not yet in use. Should work fine, even with products
+# already in it.
+
+go_to_admin($sel);
+$sel->click_ok("link=Classifications");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select classification");
+$sel->click_ok("link=cone");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit classification");
+$sel->type_ok("classification", "Unclassified");
+$sel->click_ok("//input[\@value='Update']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Classification Already Exists");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit classification");
+$sel->type_ok("classification", "ctwo");
+$sel->click_ok("//input[\@value='Update']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Classification Updated");
+
+# Now try to delete the 'ctwo' classification. It should fail as there are
+# products in it.
+
+go_to_admin($sel);
+$sel->click_ok("link=Classifications");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select classification");
+$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Error");
+my $error = trim($sel->get_text("error_msg"));
+ok($error =~ /there are products for this classification/, "Reject classification deletion");
+
+# Reclassify the product before deleting the classification.
+
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select classification");
+$sel->click_ok('//a[@href="editclassifications.cgi?action=reclassify&classification=ctwo"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Reclassify products");
+$sel->add_selection_ok("myprodlist", "label=TestProduct");
+$sel->click_ok("remove_products");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Reclassify products");
+$sel->click_ok("link=edit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select classification");
+$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete classification");
+$sel->is_text_present_ok("Do you really want to delete this classification?");
+$sel->click_ok("//input[\@value='Yes, delete']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Classification Deleted");
+
+# Disable classifications and make sure you cannot edit them anymore.
+
+set_parameters($sel, { "Bug Fields" => {"useclassification-off" => undef} });
+$sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi");
+$sel->title_is("Classification Not Enabled");
+logout($sel);
diff --git a/xt/selenium/config.t b/xt/selenium/config.t
new file mode 100644 (file)
index 0000000..b999273
--- /dev/null
@@ -0,0 +1,48 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Turn on 'requirelogin' and log out.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "User Authentication" => {"requirelogin-on" => undef} });
+logout($sel);
+
+# Accessing config.cgi should display no sensitive data.
+
+$sel->open_ok("/$config->{bugzilla_installation}/config.cgi", undef, "Go to config.cgi (JS format)");
+$sel->is_text_present_ok("var status = [ ];");
+$sel->is_text_present_ok("var status_open = [ ];");
+$sel->is_text_present_ok("var status_closed = [ ];");
+$sel->is_text_present_ok("var resolution = [ ];");
+$sel->is_text_present_ok("var keyword = [ ];");
+$sel->is_text_present_ok("var platform = [ ];");
+$sel->is_text_present_ok("var severity = [ ];");
+$sel->is_text_present_ok("var field = [\n];");
+
+ok(!$sel->is_text_present("cf_"), "No custom field displayed");
+ok(!$sel->is_text_present("component["), "No component displayed");
+ok(!$sel->is_text_present("version["), "No version displayed");
+ok(!$sel->is_text_present("target_milestone["), "No target milestone displayed");
+
+# Turn on 'requirelogin' and log out.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "User Authentication" => {"requirelogin-off" => undef} });
+logout($sel);
diff --git a/xt/selenium/create_user_accounts.t b/xt/selenium/create_user_accounts.t
new file mode 100644 (file)
index 0000000..7c71273
--- /dev/null
@@ -0,0 +1,139 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Set the email regexp for new bugzilla accounts to end with @bugzilla.test.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '[^@]+@bugzilla\.test$'}} });
+logout($sel);
+
+# Create a valid account. We need to randomize the login address, because a request
+# expires after 3 days only and this test can be executed several times per day.
+my $valid_account = 'selenium-' . random_string(10) . '@bugzilla.test';
+
+$sel->click_ok("link=Home");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugzilla Main Page");
+$sel->is_text_present_ok("Open a New Account");
+$sel->click_ok("link=Open a New Account");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create a new Bugzilla account");
+$sel->type_ok("login", $valid_account);
+$sel->click_ok("send");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Request for new user account '$valid_account' submitted");
+$sel->is_text_present_ok("A confirmation email has been sent");
+
+# Try creating the same account again. It's too soon.
+$sel->click_ok("link=Home");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugzilla Main Page");
+$sel->is_text_present_ok("Open a New Account");
+$sel->click_ok("link=Open a New Account");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create a new Bugzilla account");
+$sel->type_ok("login", $valid_account);
+$sel->click_ok("send");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Too Soon For New Token");
+my $error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /Please wait 10 minutes/, "Too soon for this account");
+
+# These accounts do not pass the regexp.
+my @accounts = ('test@yahoo.com', 'test@bugzilla.net', 'test@bugzilla.test.com');
+foreach my $account (@accounts) {
+    $sel->click_ok("link=New Account");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Create a new Bugzilla account");
+    $sel->type_ok("login", $account);
+    $sel->click_ok("send");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Account Creation Restricted");
+    $sel->is_text_present_ok("User account creation has been restricted.");
+}
+
+# These accounts are illegal.
+@accounts = ('test\bugzilla@bugzilla.test', 'test@bugzilla.org@bugzilla.test', 'test@bugzilla..test');
+# Logins larger than 127 characters must be rejected, for security reasons.
+push  @accounts, 'selenium-' . random_string(110) . '@bugzilla.test';
+
+foreach my $account (@accounts) {
+    $sel->click_ok("link=New Account");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Create a new Bugzilla account");
+    # Starting with 5.0, the login field is a type=email and is marked "required"
+    # This means that we need to add the novalidate attribute to the enclosing form
+    # so that the illegal login can still be checked by the backend code.
+    my $script = q{
+        document.getElementById('account_creation_form').setAttribute('novalidate', 1);
+    };
+    $sel->run_script($script);
+    $sel->type_ok("login", $account);
+    $sel->click_ok("send");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Invalid Email Address");
+    my $error_msg = trim($sel->get_text("error_msg"));
+    ok($error_msg =~ /^The e-mail address you entered (\S+) didn't pass our syntax checking/, "Invalid email address detected");
+}
+
+# This account already exists.
+$sel->click_ok("link=New Account");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create a new Bugzilla account");
+$sel->type_ok("login", $config->{admin_user_login});
+$sel->click_ok("send");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Account Already Exists");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg eq "There is already an account with the login name $config->{admin_user_login}.", "Account already exists");
+
+# Turn off user account creation.
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => ''}} });
+logout($sel);
+
+# Make sure that links pointing to createaccount.cgi are all deactivated.
+ok(!$sel->is_text_present("New Account"), "No link named 'New Account'");
+$sel->click_ok("link=Home");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugzilla Main Page");
+ok(!$sel->is_text_present("Open a New Account"), "No link named 'Open a New Account'");
+$sel->open_ok("/$config->{bugzilla_installation}/createaccount.cgi");
+$sel->title_is("Account Creation Disabled");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /^User account creation has been disabled. New accounts must be created by an administrator/,
+   "User account creation disabled");
+
+# Re-enable user account creation.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '.*'}} });
+
+# Make sure selenium-<random_string>@bugzilla.test has not be added to the DB yet.
+go_to_admin($sel);
+$sel->click_ok("link=Users");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search users");
+$sel->type_ok("matchstr", $valid_account);
+$sel->click_ok("search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select user");
+$sel->is_text_present_ok("0 users found");
+logout($sel);
diff --git a/xt/selenium/custom_fields.t b/xt/selenium/custom_fields.t
new file mode 100644 (file)
index 0000000..6c0c8fa
--- /dev/null
@@ -0,0 +1,462 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+log_in($sel, $config, 'admin');
+
+# Create new bug to test custom fields
+
+file_bug_in_product($sel, 'TestProduct');
+my $bug_summary = "What's your ID?";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Use the ID of this bug to generate a unique custom field name.");
+$sel->type_ok("bug_severity", "label=normal");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# Create custom fields
+
+go_to_admin($sel);
+$sel->click_ok("link=Custom Fields");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Fields");
+$sel->click_ok("link=Add a new custom field");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add a new Custom Field");
+$sel->type_ok("name", "cf_qa_freetext_$bug1_id");
+$sel->type_ok("desc", "Freetext$bug1_id");
+$sel->select_ok("type", "label=Free Text");
+$sel->type_ok("sortkey", $bug1_id);
+# These values are off by default.
+$sel->value_is("enter_bug", "off");
+$sel->value_is("obsolete", "off");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Field Created");
+$sel->is_text_present_ok("The new custom field 'cf_qa_freetext_$bug1_id' has been successfully created.");
+
+$sel->click_ok("link=Add a new custom field");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add a new Custom Field");
+$sel->type_ok("name", "cf_qa_list_$bug1_id");
+$sel->type_ok("desc", "List$bug1_id");
+$sel->select_ok("type", "label=Drop Down");
+$sel->type_ok("sortkey", $bug1_id);
+$sel->click_ok("enter_bug");
+$sel->value_is("enter_bug", "on");
+$sel->click_ok("new_bugmail");
+$sel->value_is("new_bugmail", "on");
+$sel->value_is("obsolete", "off");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Field Created");
+$sel->is_text_present_ok("The new custom field 'cf_qa_list_$bug1_id' has been successfully created.");
+
+$sel->click_ok("link=Add a new custom field");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add a new Custom Field");
+$sel->type_ok("name", "cf_qa_bugid_$bug1_id");
+$sel->type_ok("desc", "Reference$bug1_id");
+$sel->select_ok("type", "label=Bug ID");
+$sel->type_ok("sortkey", $bug1_id);
+$sel->type_ok("reverse_desc", "IsRef$bug1_id");
+$sel->click_ok("enter_bug");
+$sel->value_is("enter_bug", "on");
+$sel->value_is("obsolete", "off");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Field Created");
+$sel->is_text_present_ok("The new custom field 'cf_qa_bugid_$bug1_id' has been successfully created.");
+
+# Add values to the custom fields.
+
+$sel->click_ok("link=cf_qa_list_$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)");
+$sel->click_ok("link=Edit legal values for this field");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->type_ok("value", "have fun?");
+$sel->type_ok("sortkey", "805");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Field Value Created");
+$sel->is_text_present_ok("The value have fun? has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field.");
+
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->type_ok("value", "storage");
+$sel->type_ok("sortkey", "49");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Field Value Created");
+$sel->is_text_present_ok("The value storage has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field.");
+
+# Also create a new bug status and a new resolution.
+
+go_to_admin($sel);
+$sel->click_ok("link=Field Values");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit values for which field?");
+$sel->click_ok("link=Resolution");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'Resolution' (resolution) field");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Value for the 'Resolution' (resolution) field");
+$sel->type_ok("value", "UPSTREAM");
+$sel->type_ok("sortkey", 450);
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Field Value Created");
+
+go_to_admin($sel);
+$sel->click_ok("link=Field Values");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit values for which field?");
+$sel->click_ok("link=Status");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'Status' (bug_status) field");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Value for the 'Status' (bug_status) field");
+$sel->type_ok("value", "SUSPENDED");
+$sel->type_ok("sortkey", 250);
+$sel->click_ok("open_status");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Field Value Created");
+
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Value for the 'Status' (bug_status) field");
+$sel->type_ok("value", "IN_QA");
+$sel->type_ok("sortkey", 550);
+$sel->click_ok("closed_status");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Field Value Created");
+
+$sel->click_ok("link=status workflow page");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Workflow");
+$sel->click_ok('//td[@title="From UNCONFIRMED to SUSPENDED"]//input[@type="checkbox"]');
+$sel->click_ok('//td[@title="From CONFIRMED to SUSPENDED"]//input[@type="checkbox"]');
+$sel->click_ok('//td[@title="From SUSPENDED to CONFIRMED"]//input[@type="checkbox"]');
+$sel->click_ok('//td[@title="From SUSPENDED to IN_PROGRESS"]//input[@type="checkbox"]');
+$sel->click_ok('//td[@title="From RESOLVED to IN_QA"]//input[@type="checkbox"]');
+$sel->click_ok('//td[@title="From IN_QA to VERIFIED"]//input[@type="checkbox"]');
+$sel->click_ok('//td[@title="From IN_QA to CONFIRMED"]//input[@type="checkbox"]');
+$sel->click_ok('//input[@value="Commit Changes"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Workflow");
+
+# Create new bug to test custom fields in bug creation page
+
+file_bug_in_product($sel, 'TestProduct');
+$sel->is_text_present_ok("List$bug1_id:");
+$sel->is_element_present_ok("cf_qa_list_$bug1_id");
+$sel->is_text_present_ok("Reference$bug1_id:");
+$sel->is_element_present_ok("cf_qa_bugid_$bug1_id");
+ok(!$sel->is_text_present("Freetext$bug1_id:"), "Freetext$bug1_id is not displayed");
+ok(!$sel->is_element_present("cf_qa_freetext_$bug1_id"), "cf_qa_freetext_$bug1_id is not available");
+my $bug_summary2 = "Et de un";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->select_ok("bug_severity", "critical");
+$sel->type_ok("cf_qa_bugid_$bug1_id", $bug1_id);
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+# Both fields are editable.
+
+$sel->type_ok("cf_qa_freetext_$bug1_id", "bonsai");
+$sel->selected_label_is("cf_qa_list_$bug1_id", "---");
+$sel->select_ok("bug_status", "label=SUSPENDED");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+go_to_bug($sel, $bug1_id);
+$sel->type_ok("cf_qa_freetext_$bug1_id", "dumbo");
+$sel->select_ok("cf_qa_list_$bug1_id", "label=storage");
+$sel->is_text_present_ok("IsRef$bug1_id: $bug2_id");
+$sel->select_ok("bug_status", "RESOLVED");
+$sel->select_ok("resolution", "UPSTREAM");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->select_ok("bug_status", "IN_QA");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+$sel->click_ok("link=Format For Printing");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Full Text Bug Listing");
+$sel->is_text_present_ok("Freetext$bug1_id: dumbo");
+$sel->is_text_present_ok("List$bug1_id: storage");
+$sel->is_text_present_ok("Status: IN_QA UPSTREAM");
+go_to_bug($sel, $bug2_id);
+$sel->select_ok("cf_qa_list_$bug1_id", "label=storage");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+# Test searching for bugs using the custom fields
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections_ok("product");
+$sel->add_selection_ok("product", "TestProduct");
+$sel->remove_all_selections("bug_status");
+$sel->remove_all_selections("resolution");
+$sel->select_ok("f1", "label=List$bug1_id");
+$sel->select_ok("o1", "label=is equal to");
+$sel->type_ok("v1", "storage");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("2 bugs found");
+$sel->is_text_present_ok("What's your ID?");
+$sel->is_text_present_ok("Et de un");
+
+# Now edit custom fields in mass changes.
+
+$sel->click_ok("mass_change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->click_ok("check_all");
+$sel->select_ok("cf_qa_list_$bug1_id", "label=---");
+$sel->type_ok("cf_qa_freetext_$bug1_id", "thanks");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugs processed");
+$sel->click_ok("link=$bug2_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug2_id/);
+$sel->value_is("cf_qa_freetext_$bug1_id", "thanks");
+$sel->selected_label_is("cf_qa_list_$bug1_id", "---");
+$sel->select_ok("cf_qa_list_$bug1_id", "label=storage");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+# Let's now test custom field visibility.
+
+go_to_admin($sel);
+$sel->click_ok("link=Custom Fields");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Fields");
+$sel->click_ok("link=cf_qa_list_$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)");
+$sel->select_ok("visibility_field_id", "label=Severity (bug_severity)");
+$sel->add_selection_ok("visibility_values", "label=blocker");
+$sel->add_selection_ok("visibility_values", "label=critical");
+$sel->click_ok("edit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Field Updated");
+
+go_to_bug($sel, $bug1_id);
+$sel->is_element_present_ok("cf_qa_list_$bug1_id", "List$bug1_id is in the DOM of the page...");
+ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... but is not displayed with severity = 'normal'");
+$sel->select_ok("bug_severity", "major");
+ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... nor with severity = 'major'");
+$sel->select_ok("bug_severity", "critical");
+$sel->is_visible_ok("cf_qa_list_$bug1_id", "... but is visible with severity = 'critical'");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->is_visible_ok("cf_qa_list_$bug1_id");
+
+go_to_bug($sel, $bug2_id);
+$sel->is_visible_ok("cf_qa_list_$bug1_id");
+$sel->select_ok("bug_severity", "minor");
+ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'");
+edit_bug_and_return($sel, $bug2_id, $bug_summary2);
+ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'");
+
+# Add a new value which is only listed under some condition.
+
+go_to_admin($sel);
+$sel->click_ok("link=Custom Fields");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Fields");
+$sel->click_ok("link=cf_qa_list_$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)");
+$sel->select_ok("value_field_id", "label=Resolution (resolution)");
+$sel->click_ok("edit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Field Updated");
+$sel->click_ok("link=cf_qa_list_$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)");
+$sel->click_ok("link=Edit legal values for this field");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->type_ok("value", "ghost");
+$sel->type_ok("sortkey", "500");
+$sel->select_ok("visibility_value_id", "label=FIXED");
+$sel->click_ok("id=create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Field Value Created");
+
+go_to_bug($sel, $bug1_id);
+my @labels = $sel->get_select_options("cf_qa_list_$bug1_id");
+ok(grep(/^ghost$/, @labels), "ghost is in the DOM of the page...");
+my $disabled = $sel->get_attribute("v4_cf_qa_list_$bug1_id\@disabled");
+ok($disabled, "... but is not available for selection by default");
+$sel->select_ok("bug_status", "label=RESOLVED");
+$sel->select_ok("resolution", "label=FIXED");
+$sel->select_ok("cf_qa_list_$bug1_id", "label=ghost");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->selected_label_is("cf_qa_list_$bug1_id", "ghost");
+
+# Delete an unused field value.
+
+go_to_admin($sel);
+$sel->click_ok("link=Field Values");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit values for which field?");
+$sel->click_ok("link=List$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=have%20fun%3F')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Value 'have fun?' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->is_text_present_ok("Do you really want to delete this value?");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Field Value Deleted");
+
+# This value cannot be deleted as it's in use.
+
+$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=storage')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Value 'storage' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field");
+$sel->is_text_present_ok("There is 1 bug with this field value");
+
+# Mark the <select> field as obsolete, making it unavailable in bug reports.
+
+go_to_admin($sel);
+$sel->click_ok("link=Custom Fields");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Fields");
+$sel->click_ok("link=cf_qa_list_$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)");
+$sel->click_ok("obsolete");
+$sel->value_is("obsolete", "on");
+$sel->click_ok("edit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Field Updated");
+go_to_bug($sel, $bug1_id);
+$sel->value_is("cf_qa_freetext_$bug1_id", "thanks");
+ok(!$sel->is_element_present("cf_qa_list_$bug1_id"), "The custom list is not visible");
+
+# Custom fields are also viewable by logged out users.
+
+logout($sel);
+go_to_bug($sel, $bug1_id);
+$sel->is_text_present_ok("Freetext$bug1_id: thanks");
+
+# Powerless users should still be able to CC themselves when
+# custom fields are in use.
+
+log_in($sel, $config, 'unprivileged');
+go_to_bug($sel, $bug1_id);
+$sel->is_text_present_ok("Freetext$bug1_id: thanks");
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", $config->{unprivileged_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
+
+# Disable the remaining free text field.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Custom Fields");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Fields");
+$sel->click_ok("link=cf_qa_freetext_$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit the Custom Field 'cf_qa_freetext_$bug1_id' (Freetext$bug1_id)");
+$sel->click_ok("obsolete");
+$sel->value_is("obsolete", "on");
+$sel->click_ok("edit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Field Updated");
+
+# Trying to delete a bug status which is in use is forbidden.
+
+go_to_admin($sel);
+$sel->click_ok("link=Field Values");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit values for which field?");
+$sel->click_ok("link=Status");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'Status' (bug_status) field");
+$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=bug_status&value=SUSPENDED"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Value 'SUSPENDED' from the 'Status' (bug_status) field");
+$sel->is_text_present_ok("Sorry, but the 'SUSPENDED' value cannot be deleted");
+
+go_to_bug($sel, $bug2_id);
+$sel->select_ok("bug_status", "CONFIRMED");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+go_to_bug($sel, $bug1_id);
+$sel->select_ok("bug_status", "VERIFIED");
+$sel->select_ok("resolution", "INVALID");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# Unused values can be deleted.
+
+go_to_admin($sel);
+$sel->click_ok("link=Field Values");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit values for which field?");
+$sel->click_ok("link=Status");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'Status' (bug_status) field");
+$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=bug_status&value=SUSPENDED"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Value 'SUSPENDED' from the 'Status' (bug_status) field");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Field Value Deleted");
+$sel->is_text_present_ok("The value SUSPENDED of the Status (bug_status) field has been deleted");
+
+$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=bug_status&value=IN_QA"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Value 'IN_QA' from the 'Status' (bug_status) field");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Field Value Deleted");
+$sel->is_text_present_ok("The value IN_QA of the Status (bug_status) field has been deleted");
+
+go_to_admin($sel);
+$sel->click_ok("link=Field Values");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit values for which field?");
+$sel->click_ok("link=Resolution");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select value for the 'Resolution' (resolution) field");
+$sel->click_ok('//a[@href="editvalues.cgi?action=del&field=resolution&value=UPSTREAM"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Value 'UPSTREAM' from the 'Resolution' (resolution) field");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Field Value Deleted");
+$sel->is_text_present_ok("The value UPSTREAM of the Resolution (resolution) field has been deleted");
+
+logout($sel);
diff --git a/xt/selenium/custom_fields_admin.t b/xt/selenium/custom_fields_admin.t
new file mode 100644 (file)
index 0000000..d0ffb9d
--- /dev/null
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+log_in($sel, $config, 'admin');
+
+# Create a custom field, going through each type available,
+# mark it as obsolete and delete it immediately.
+
+go_to_admin($sel);
+$sel->click_ok("link=Custom Fields");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Custom Fields");
+
+my @types = ("Bug ID", "Large Text Box", "Free Text", "Multiple-Selection Box",
+             "Drop Down", "Date/Time");
+my $counter = int(rand(10000));
+
+foreach my $type (@types) {
+    my $fname = "cf_field" . ++$counter;
+    my $fdesc = "Field" . $counter;
+
+    $sel->click_ok("link=Add a new custom field");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Add a new Custom Field");
+    $sel->type_ok("name", $fname);
+    $sel->type_ok("desc", $fdesc);
+    $sel->select_ok("type", "label=$type");
+    $sel->click_ok("obsolete");
+    $sel->click_ok("create");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Custom Field Created");
+    $sel->click_ok("//a[\@href='editfields.cgi?action=del&name=$fname']");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Delete the Custom Field '$fname' ($fdesc)");
+    $sel->click_ok("link=Delete field '$fdesc'");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Custom Field Deleted");
+}
+
+logout($sel);
diff --git a/xt/selenium/dependencies.t b/xt/selenium/dependencies.t
new file mode 100644 (file)
index 0000000..133e17e
--- /dev/null
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Let's create a public and a private bug.
+
+log_in($sel, $config, 'admin');
+file_bug_in_product($sel, "TestProduct");
+my $bug_summary = "Dependency Checks";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "This bug is public");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+file_bug_in_product($sel, "TestProduct");
+$sel->type_ok("alias", "secret_qa_bug_$bug1_id+1");
+my $bug_summary2 = "Big Ben";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "This bug is private");
+$sel->type_ok("dependson", $bug1_id);
+$sel->check_ok('//input[@name="groups" and @value="Master"]');
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("link=Mark as Duplicate");
+$sel->type_ok("dup_id", $bug2_id);
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->is_text_present_ok("secret_qa_bug_$bug1_id+1");
+logout($sel);
+
+# A user with editbugs privs who cannot see some bugs in the dependency list
+# or the bug this duplicate points to should still be able to edit this bug.
+
+log_in($sel, $config, 'editbugs');
+go_to_bug($sel, $bug1_id);
+ok(!$sel->is_text_present("secret_qa_bug_$bug1_id+1"), "The alias of the private bug is not visible");
+$sel->select_ok("priority", "label=High");
+$sel->select_ok("bug_status", "VERIFIED");
+$sel->type_ok("comment", "Can I still edit this bug?");
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
diff --git a/xt/selenium/edit_products_properties.t b/xt/selenium/edit_products_properties.t
new file mode 100644 (file)
index 0000000..1f98517
--- /dev/null
@@ -0,0 +1,338 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+my $admin_user_login = $config->{admin_user_login};
+my $unprivileged_user_login = $config->{unprivileged_user_login};
+my $permanent_user = $config->{permanent_user};
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields"              => {"useclassification-off" => undef,
+                                                     "usetargetmilestone-on" => undef},
+                       "Administrative Policies" => {"allowbugdeletion-on"   => undef}
+                     });
+
+# Create a product and add components to it. Do some cleanup first
+# if the script failed during a previous run.
+
+go_to_admin($sel);
+$sel->click_ok("link=Products");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+# No risk to get the "Select classification" page. We turned off useclassification.
+$sel->title_is("Select product");
+
+my $text = trim($sel->get_text("bugzilla-body"));
+if ($text =~ /(Kill me!|Kill me nicely)/) {
+    my $product = $1;
+    my $escaped_product = url_quote($product);
+    $sel->click_ok("//a[\@href='editproducts.cgi?action=del&product=$escaped_product']");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Delete Product '$product'");
+    $sel->click_ok("delete");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Product Deleted");
+}
+
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Product");
+$sel->type_ok("product", "Kill me!");
+$sel->type_ok("description", "I will disappear very soon. Do not add bugs to it.");
+$sel->type_ok("defaultmilestone", "0.1a");
+# Since Bugzilla 4.0, the voting system is in an extension.
+if ($config->{test_extensions}) {
+    $sel->type_ok("votesperuser", "1");
+    $sel->type_ok("maxvotesperbug", "1");
+    $sel->type_ok("votestoconfirm", "10");
+}
+$sel->type_ok("version", "0.1a");
+$sel->type_ok("component", "first comp");
+$sel->type_ok("comp_desc", "comp 1");
+$sel->type_ok("initialowner", $admin_user_login);
+$sel->click_ok("add-product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Created");
+
+# Try creating a second component with the same name.
+
+$sel->click_ok("link=Edit components:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select component of product 'Kill me!'");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add component to the Kill me! product");
+$sel->type_ok("component", "first comp");
+$sel->type_ok("description", "comp 2");
+$sel->type_ok("initialowner", $admin_user_login);
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Already Exists");
+
+# Now really create a second component, with a distinct name.
+
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->type_ok("component", "second comp");
+# FIXME - Re-enter the default assignee (regression due to bug 577574)
+$sel->type_ok("initialowner", $admin_user_login);
+$sel->type_ok("initialcc", $permanent_user);
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Created");
+
+# Add a new version.
+
+edit_product($sel, "Kill me!");
+$sel->click_ok("link=Edit versions:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select version of product 'Kill me!'");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->type_ok("version", "0.1");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Version Created");
+
+# Add a new milestone.
+
+$sel->click_ok("link='Kill me!'");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'Kill me!'");
+$sel->click_ok("link=Edit milestones:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select milestone of product 'Kill me!'");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Milestone to Product 'Kill me!'");
+$sel->type_ok("milestone", "0.2");
+$sel->type_ok("sortkey", "2");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Created");
+
+# Add another milestone.
+
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Milestone to Product 'Kill me!'");
+$sel->type_ok("milestone", "0.1a");
+# Negative sortkeys are valid for milestones.
+$sel->type_ok("sortkey", "-2");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Already Exists");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->type_ok("milestone", "pre-0.1");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Created");
+
+# Now create an UNCONFIRMED bug and add it to the newly created product.
+
+file_bug_in_product($sel, "Kill me!");
+$sel->select_ok("version", "label=0.1a");
+$sel->select_ok("component", "label=first comp");
+# UNCONFIRMED must be present.
+$sel->select_ok("bug_status", "label=UNCONFIRMED");
+$sel->type_ok("cc", $unprivileged_user_login);
+$sel->type_ok("bug_file_loc", "http://www.test.com");
+my $bug_summary = "test create/edit product properties";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "this bug will soon be dead");
+my $bug1_id = create_bug($sel, $bug_summary);
+my @cc_list = $sel->get_select_options("cc");
+ok(grep($_ eq $unprivileged_user_login, @cc_list), "$unprivileged_user_login correctly added to the CC list");
+ok(!grep($_ eq $permanent_user, @cc_list), "$permanent_user not in the CC list for 'first comp' by default");
+
+# File a second bug, and make sure users in the default CC list are added.
+file_bug_in_product($sel, "Kill me!");
+$sel->select_ok("version", "label=0.1a");
+$sel->select_ok("component", "label=second comp");
+my $bug_summary2 = "check default CC list";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "is the CC list populated correctly?");
+create_bug($sel, $bug_summary2);
+@cc_list = $sel->get_select_options("cc");
+ok(grep($_ eq $permanent_user, @cc_list), "$permanent_user in the CC list for 'second comp' by default");
+
+# Edit product properties and set votes_to_confirm to 0, which has
+# the side-effect to disable auto-confirmation (new behavior compared
+# to Bugzilla 3.4 and older).
+
+edit_product($sel, "Kill me!");
+$sel->type_ok("product", "Kill me nicely");
+$sel->type_ok("description", "I will disappear very soon. Do not add bugs to it (except for testing).");
+$sel->select_ok("defaultmilestone", "label=0.2");
+if ($config->{test_extensions}) {
+    $sel->type_ok("votesperuser", "2");
+    $sel->type_ok("maxvotesperbug", 5);
+    $sel->type_ok("votestoconfirm", "0");
+}
+$sel->click_ok("update-product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Updating Product 'Kill me nicely'");
+$sel->is_text_present_ok("Updated product name from 'Kill me!' to 'Kill me nicely'");
+$sel->is_text_present_ok("Updated description");
+$sel->is_text_present_ok("Updated default milestone");
+if ($config->{test_extensions}) {
+    $sel->is_text_present_ok("Updated votes per user");
+    $sel->is_text_present_ok("Updated maximum votes per bug");
+    $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
+    $text = trim($sel->get_text("bugzilla-body"));
+    # We use .{1} in place of the right arrow character, which fails otherwise.
+    ok($text =~ /Checking unconfirmed bugs in this product for any which now have sufficient votes\.{3} .{1}there were none/,
+       "No bugs confirmed by popular votes (votestoconfirm = 0 disables auto-confirmation)");
+
+    # Now set votestoconfirm to 2, vote for a bug, and then set
+    # this attribute back to 1, to trigger auto-confirmation.
+
+    $sel->click_ok("link=Kill me nicely");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Edit Product 'Kill me nicely'", "Display properties of Kill me nicely");
+    $sel->type_ok("votestoconfirm", 2);
+    $sel->click_ok("update-product");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Updating Product 'Kill me nicely'");
+    $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
+
+    go_to_bug($sel, $bug1_id);
+    $sel->click_ok("link=vote");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Change Votes");
+    $sel->type_ok("bug_$bug1_id", 1);
+    $sel->click_ok("change");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Change Votes");
+    $sel->is_text_present_ok("The changes to your votes have been saved");
+
+    edit_product($sel, "Kill me nicely");
+    $sel->type_ok("votestoconfirm", 1);
+    $sel->click_ok("update-product");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Updating Product 'Kill me nicely'");
+    $sel->is_text_present_ok("Updated number of votes needed to confirm a bug");
+    $text = trim($sel->get_text("bugzilla-body"));
+    ok($text =~ /Bug $bug1_id confirmed by number of votes/, "Bug $bug1_id is confirmed by popular votes");
+}
+
+# Edit the bug.
+
+go_to_bug($sel, $bug1_id);
+$sel->selected_label_is("product", "Kill me nicely");
+$sel->selected_label_is("bug_status", "CONFIRMED") if $config->{test_extensions};
+$sel->select_ok("target_milestone", "label=pre-0.1");
+$sel->select_ok("component", "label=second comp");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+@cc_list = $sel->get_select_options("cc");
+ok(grep($_ eq $permanent_user, @cc_list), "User $permanent_user automatically added to the CC list");
+
+# Delete the milestone the bug belongs to. This should retarget the bug
+# to the default milestone.
+
+edit_product($sel, "Kill me nicely");
+$sel->click_ok("link=Edit milestones:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select milestone of product 'Kill me nicely'");
+$sel->click_ok('//a[@href="editmilestones.cgi?action=del&product=Kill%20me%20nicely&milestone=pre-0.1"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Milestone of Product 'Kill me nicely'");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /There is 1 bug entered for this milestone/, "Warning displayed");
+ok($text =~ /Do you really want to delete this milestone\?/, "Requesting confirmation");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Deleted");
+$text = trim($sel->get_text("message"));
+ok($text =~ /Bugs targetted to this milestone have been retargetted to the default milestone/, "Bug retargetted");
+
+# Try deleting the version used by the bug. This action must be rejected.
+
+$sel->click_ok("link='Kill me nicely'");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'Kill me nicely'");
+$sel->click_ok("link=Edit versions:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select version of product 'Kill me nicely'");
+$sel->click_ok("//a[contains(\@href, 'editversions.cgi?action=del&product=Kill%20me%20nicely&version=0.1a')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Version of Product 'Kill me nicely'");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /Sorry, there are 2 outstanding bugs for this version/, "Rejecting version deletion");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+
+# Delete an unused version. The action must succeed.
+
+$sel->click_ok('//a[@href="editversions.cgi?action=del&product=Kill%20me%20nicely&version=0.1"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Version of Product 'Kill me nicely'");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /Do you really want to delete this version\?/, "Requesting confirmation");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Version Deleted");
+
+# Delete the component the bug belongs to. The action must succeed.
+
+$sel->click_ok("link='Kill me nicely'");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'Kill me nicely'");
+$sel->click_ok("link=Edit components:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select component of product 'Kill me nicely'");
+$sel->click_ok("//a[contains(\@href, 'editcomponents.cgi?action=del&product=Kill%20me%20nicely&component=second%20comp')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete component 'second comp' from 'Kill me nicely' product");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /There are 2 bugs entered for this component/, "Warning displayed");
+ok($text =~ /Do you really want to delete this component\?/, "Requesting confirmation");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Deleted");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /The component second comp has been deleted/, "Component deletion confirmed");
+ok($text =~ /All bugs being in this component and all references to them have also been deleted/,
+   "Bug deletion confirmed");
+
+# Only one value for component, version and milestone available. They should
+# be selected by default.
+
+file_bug_in_product($sel, "Kill me nicely");
+$bug_summary2 = "bye bye everybody!";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "I'm dead :(");
+create_bug($sel, $bug_summary2);
+
+# Now delete the product.
+
+go_to_admin($sel);
+$sel->click_ok("link=Products");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select product");
+$sel->click_ok("//a[\@href='editproducts.cgi?action=del&product=Kill%20me%20nicely']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Product 'Kill me nicely'");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /There is 1 bug entered for this product/, "Warning displayed");
+ok($text =~ /Do you really want to delete this product\?/, "Confirmation request displayed");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Deleted");
+logout($sel);
diff --git a/xt/selenium/email_preferences.t b/xt/selenium/email_preferences.t
new file mode 100644 (file)
index 0000000..c1e60b0
--- /dev/null
@@ -0,0 +1,405 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Used to test sent bugmails
+use constant RCPT_BOTH   => 1;
+use constant RCPT_ADMIN  => 2;
+use constant RCPT_NORMAL => 3;
+use constant RCPT_NONE   => 4;
+my @email_both = ($config->{admin_user_login}, $config->{editbugs_user_login});
+my @email_admin = ($config->{admin_user_login});
+my @email_normal = ($config->{editbugs_user_login});
+my @email_none = ("no one");
+
+# Test script to test email preferences.
+# For reference, following bugmail and request mails should be generated.
+#
+# Admin should get following bugmails (in order):
+#  1) A bug is created
+#  2) Normal user adds a CC for itself
+#  3) Admin removes CC of normal user
+#  4) Admin assigns the bug to itself
+#  5) Admin requests a flag from normal user
+#  6) Admin grants a flag requested from itself
+#  7) Normal user set severity to normal
+#  8) Normal user adds a comment #3
+#  9) Normal user assigns the bug to itself
+# Normal User should get following bugmail (in order):
+#  1) A bug is created
+#  2) Normal user sets severity to blocker
+#  3) Admin sets severity to trivial
+#  4) Admin adds a comment #2
+#  5) Admin removes CC of normal user
+#  6) Admin assigns the bug to itself
+#  7) Normal user sets severity to normal
+#
+# Admin should get following request mails (in order):
+#  1) Normal user denies a flag requested by the admin
+# Normal user should get following request mails (in order):
+#  1) Admin requests a flag from normal user
+#
+# NOTE that only correct bugmail is verified by the test script because
+# sending request mail is not indicated on the UI.
+
+# Set admin Email Prefs (via link in footer)
+log_in($sel, $config, 'admin');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Email Notifications");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Email Notifications");
+$sel->click_ok("//input[\@value='Disable All Mail']");
+$sel->click_ok("email-0-1", undef, 'Set "I\'m added to or removed from this capacity" for Assignee role');
+$sel->click_ok("email-0-5", undef, 'Set "The priority, status, severity, or milestone changes" for Assignee role');
+$sel->click_ok("email-0-2", undef, 'Set "New comments are added" for Assignee role');
+$sel->click_ok("email-0-0", undef, 'Set "Any field not mentioned above changes" for Assignee role');
+$sel->click_ok("email-3-8", undef, 'Set "The CC field changes" for CCed role');
+$sel->click_ok("email-1-10", undef, 'Set "A new bug is created" for QA Contact role');
+$sel->click_ok("email-100-101", undef, 'Set "Email me when someone sets a flag I asked for" global option');
+# Restore the old 4.2 behavior for 'Disable All Mail'.
+foreach my $col (0..3) {
+    foreach my $row (50..51) {
+        $sel->click_ok("neg-email-$col-$row");
+    }
+}
+$sel->value_is("email-0-1", "on");
+$sel->value_is("email-0-10", "off");
+$sel->value_is("email-0-6", "off");
+$sel->value_is("email-0-5", "on");
+$sel->value_is("email-0-2", "on");
+$sel->value_is("email-0-3", "off");
+$sel->value_is("email-0-4", "off");
+$sel->value_is("email-0-7", "off");
+$sel->value_is("email-0-8", "off");
+$sel->value_is("email-0-9", "off");
+$sel->value_is("email-0-0", "on");
+$sel->value_is("neg-email-0-50", "off");
+$sel->value_is("neg-email-0-51", "off");
+$sel->value_is("email-1-1", "off");
+$sel->value_is("email-1-10", "on");
+$sel->value_is("email-1-6", "off");
+$sel->value_is("email-1-5", "off");
+$sel->value_is("email-1-2", "off");
+$sel->value_is("email-1-3", "off");
+$sel->value_is("email-1-4", "off");
+$sel->value_is("email-1-7", "off");
+$sel->value_is("email-1-8", "off");
+$sel->value_is("email-1-9", "off");
+$sel->value_is("email-1-0", "off");
+$sel->value_is("neg-email-1-50", "off");
+$sel->value_is("neg-email-1-51", "off");
+ok(!$sel->is_editable("email-2-1"), 'The "I\'m added to or removed from this capacity" for Reporter role is disabled');
+$sel->value_is("email-2-10", "off");
+$sel->value_is("email-2-6", "off");
+$sel->value_is("email-2-5", "off");
+$sel->value_is("email-2-2", "off");
+$sel->value_is("email-2-3", "off");
+$sel->value_is("email-2-4", "off");
+$sel->value_is("email-2-7", "off");
+$sel->value_is("email-2-8", "off");
+$sel->value_is("email-2-9", "off");
+$sel->value_is("email-2-0", "off");
+$sel->value_is("neg-email-2-50", "off");
+$sel->value_is("neg-email-2-51", "off");
+$sel->value_is("email-3-1", "off");
+$sel->value_is("email-3-10", "off");
+$sel->value_is("email-3-6", "off");
+$sel->value_is("email-3-5", "off");
+$sel->value_is("email-3-2", "off");
+$sel->value_is("email-3-3", "off");
+$sel->value_is("email-3-4", "off");
+$sel->value_is("email-3-7", "off");
+$sel->value_is("email-3-8", "on");
+$sel->value_is("email-3-9", "off");
+$sel->value_is("email-3-0", "off");
+$sel->value_is("neg-email-3-50", "off");
+$sel->value_is("neg-email-3-51", "off");
+$sel->value_is("email-100-100", "off");
+$sel->value_is("email-100-101", "on");
+$sel->click_ok("update", undef, "Submit modified admin email preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("The changes to your email notifications have been saved.");
+
+# Set "After changing a bug" default preference to "Show the updated bug"
+# This simplifies bug changes below
+go_to_admin($sel);
+$sel->click_ok("link=Default Preferences");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+$sel->select_ok("post_bug_submit_action", "label=Show the updated bug");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+
+# Set normal user Email Prefs
+logout($sel);
+log_in($sel, $config, 'editbugs');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Email Notifications");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Email Notifications");
+$sel->is_text_present_ok("Email Notifications");
+$sel->click_ok("//input[\@value='Enable All Mail']");
+$sel->click_ok("email-3-1", undef, 'Clear "I\'m added to or removed from this capacity" for CCed role');
+$sel->click_ok("email-3-5", undef, 'Clear "The priority, status, severity, or milestone changes" for CCed role');
+$sel->click_ok("email-2-2", undef, 'Clear "New comments are added" for Reporter role');
+$sel->click_ok("email-3-2", undef, 'Clear "New comments are added" for CCed role');
+$sel->click_ok("email-2-8", undef, 'Clear "The CC field changes" for Reporter role');
+$sel->click_ok("email-3-8", undef, 'Clear "The CC field changes" for CCed role');
+$sel->click_ok("email-2-0", undef, 'Clear "Any field not mentioned above changes" for Reporter role');
+$sel->click_ok("email-3-0", undef, 'Clear "Any field not mentioned above changes" for CCed role');
+$sel->click_ok("neg-email-0-51", undef, 'Set "Change was made by me" override for Assignee role');
+$sel->click_ok("email-100-101", undef, 'Clear "Email me when someone sets a flag I asked for" global option');
+$sel->value_is("email-0-1", "on");
+$sel->value_is("email-0-10", "on");
+$sel->value_is("email-0-6", "on");
+$sel->value_is("email-0-5", "on");
+$sel->value_is("email-0-2", "on");
+$sel->value_is("email-0-3", "on");
+$sel->value_is("email-0-4", "on");
+$sel->value_is("email-0-7", "on");
+$sel->value_is("email-0-8", "on");
+$sel->value_is("email-0-9", "on");
+$sel->value_is("email-0-0", "on");
+$sel->value_is("neg-email-0-50", "off");
+$sel->value_is("neg-email-0-51", "on");
+$sel->value_is("email-1-1", "on");
+$sel->value_is("email-1-10", "on");
+$sel->value_is("email-1-6", "on");
+$sel->value_is("email-1-5", "on");
+$sel->value_is("email-1-2", "on");
+$sel->value_is("email-1-3", "on");
+$sel->value_is("email-1-4", "on");
+$sel->value_is("email-1-7", "on");
+$sel->value_is("email-1-8", "on");
+$sel->value_is("email-1-9", "on");
+$sel->value_is("email-1-0", "on");
+$sel->value_is("neg-email-1-50", "off");
+$sel->value_is("neg-email-1-51", "off");
+ok(!$sel->is_editable("email-2-1"), 'The "I\'m added to or removed from this capacity" for Reporter role is disabled');
+$sel->value_is("email-2-10", "on");
+$sel->value_is("email-2-6", "on");
+$sel->value_is("email-2-5", "on");
+$sel->value_is("email-2-2", "off");
+$sel->value_is("email-2-3", "on");
+$sel->value_is("email-2-4", "on");
+$sel->value_is("email-2-7", "on");
+$sel->value_is("email-2-8", "off");
+$sel->value_is("email-2-9", "on");
+$sel->value_is("email-2-0", "off");
+$sel->value_is("neg-email-2-50", "off");
+$sel->value_is("neg-email-2-51", "off");
+$sel->value_is("email-3-1", "off");
+$sel->value_is("email-3-10", "on");
+$sel->value_is("email-3-6", "on");
+$sel->value_is("email-3-5", "off");
+$sel->value_is("email-3-2", "off");
+$sel->value_is("email-3-3", "on");
+$sel->value_is("email-3-4", "on");
+$sel->value_is("email-3-7", "on");
+$sel->value_is("email-3-8", "off");
+$sel->value_is("email-3-9", "on");
+$sel->value_is("email-3-0", "off");
+$sel->value_is("neg-email-3-50", "off");
+$sel->value_is("neg-email-3-51", "off");
+$sel->value_is("email-100-100", "on");
+$sel->value_is("email-100-101", "off");
+$sel->click_ok("update", undef, "Submit modified normal user email preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("The changes to your email notifications have been saved.");
+
+# Create a test bug (bugmail to both normal user and admin)
+file_bug_in_product($sel, "Another Product");
+$sel->select_ok("component", "label=c1");
+my $bug_summary = "Selenium Email Preference test bug";
+$sel->type_ok("short_desc", $bug_summary, "Enter bug summary");
+$sel->type_ok("comment", "Created by Selenium to test Email Notifications", "Enter bug description");
+$sel->type_ok("assigned_to", $config->{editbugs_user_login});
+$sel->type_ok("qa_contact", $config->{admin_user_login});
+$sel->type_ok("cc", $config->{admin_user_login});
+my $bug1_id = create_bug($sel, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_BOTH);
+
+# Make normal user changes (first pass)
+#
+go_to_bug($sel, $bug1_id);
+# Severity change (bugmail to normal user but not admin)
+$sel->select_ok("bug_severity", "label=blocker");
+$sel->selected_label_is("bug_severity", "blocker");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_NORMAL);
+# Add a comment (bugmail to no one)
+$sel->type_ok("comment", "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)");
+$sel->value_is("comment", "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_NONE);
+# Add normal user to CC list (bugmail to admin but not normal user)
+$sel->type_ok("newcc", $config->{editbugs_user_login});
+$sel->value_is("newcc", $config->{editbugs_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_ADMIN);
+# Request a flag from admin (bugmail to no one, request mail to no one)
+$sel->select_ok("flag_type-1", "label=?");
+$sel->type_ok("requestee_type-1", $config->{admin_user_login});
+$sel->value_is("requestee_type-1", $config->{admin_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_NONE);
+
+# Make admin changes
+#
+logout($sel);
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug1_id);
+# Severity change (bugmail to normal user but not admin)
+$sel->select_ok("bug_severity", "label=trivial");
+$sel->selected_label_is("bug_severity", "trivial");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_NORMAL);
+# Add a comment (bugmail to normal user but not admin)
+$sel->type_ok("comment", "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)");
+$sel->value_is("comment", "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_NORMAL);
+# Remove normal user from CC list (bugmail to both normal user and admin)
+$sel->click_ok("removecc");
+$sel->add_selection_ok("cc", "label=$config->{editbugs_user_login}");
+$sel->value_is("removecc", "on");
+$sel->selected_label_is("cc", $config->{editbugs_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_BOTH);
+# Reassign bug to admin user (bugmail to both normal user and admin)
+$sel->type_ok("assigned_to", $config->{admin_user_login});
+$sel->value_is("assigned_to", $config->{admin_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_BOTH);
+# Request a flag from normal user (bugmail to admin but not normal user and request mail to admin)
+$sel->select_ok("flag_type-1", "label=?");
+$sel->type_ok("requestee_type-1", $config->{editbugs_user_login});
+$sel->value_is("requestee_type-1", $config->{editbugs_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_ADMIN);
+# Grant a normal user flag request (bugmail to admin but not normal user and request mail to no one)
+my $flag1_id = set_flag($sel, $config->{admin_user_login}, "?", "+");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_ADMIN);
+
+# Make normal user changes (second pass)
+#
+logout($sel);
+log_in($sel, $config, 'editbugs');
+go_to_bug($sel, $bug1_id);
+# Severity change (bugmail to both admin and normal user)
+$sel->select_ok("bug_severity", "label=normal");
+$sel->selected_label_is("bug_severity", "normal");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_BOTH);
+# Add a comment (bugmail to admin but not normal user)
+$sel->type_ok("comment", "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)");
+$sel->value_is("comment", "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_ADMIN);
+# Reassign to normal user (bugmail to admin but not normal user)
+$sel->type_ok("assigned_to", $config->{editbugs_user_login});
+$sel->value_is("assigned_to", $config->{editbugs_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_ADMIN);
+# Deny a flag requested by admin (bugmail to no one and request mail to admin)
+my $flag2_id = set_flag($sel, $config->{editbugs_user_login}, "?", "-");
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_NONE);
+# Cancel both flags (bugmail and request mail to no one)
+set_flag($sel, undef, "+", "X", $flag1_id);
+set_flag($sel, undef, "-", "X", $flag2_id);
+edit_bug($sel, $bug1_id, $bug_summary);
+verify_bugmail_recipients($sel, RCPT_NONE);
+logout($sel);
+
+# Set "After changing a bug" default preference back to "Do Nothing".
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Default Preferences");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+$sel->select_ok("post_bug_submit_action", "label=Do Nothing");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+logout($sel);
+
+# Help functions
+sub verify_bugmail_recipients {
+    my ($sel, $rcpt_sentto) = @_;
+    my $wanted_sentto;
+    my $err = 0;
+
+    # Verify sentto field
+    my @email_sentto
+        = sort split(/, /, $sel->get_text("//dt[text()='Email sent to:']/following-sibling::dd"));
+    if ($rcpt_sentto == RCPT_BOTH) {
+      $wanted_sentto = \@email_both;
+      is_deeply(\@email_sentto, $wanted_sentto, "Bugmail sent to both")
+          or $err = 1;
+    }
+    elsif ($rcpt_sentto == RCPT_ADMIN) {
+      $wanted_sentto = \@email_admin;
+      is_deeply(\@email_sentto, $wanted_sentto, "Bugmail sent to admin")
+          or $err = 1;
+    }
+    elsif ($rcpt_sentto == RCPT_NORMAL) {
+      $wanted_sentto = \@email_normal;
+      is_deeply(\@email_sentto, $wanted_sentto, "Bugmail sent to normal user")
+          or $err = 1;
+    } else {
+      $wanted_sentto = \@email_none;
+      is_deeply(\@email_sentto, $wanted_sentto, "Bugmail sent to no one")
+          or $err = 1;
+    }
+
+    # In case of an error, retrieve and show diagnostics info
+    if ($err) {
+        diag("Sent, actual     : " . join(', ', @email_sentto));
+        diag("Sent, wanted     : " . join(', ', @$wanted_sentto));
+        diag("Changer          : " . trim($sel->get_text('//a[contains(@href, "logout")]/../text()[3]')));
+        diag("Reporter         : " . $sel->get_attribute('//th[contains(text(), "Reported:")]/following-sibling::td//a@title'));
+        diag("Assignee         : " . $sel->get_value('assigned_to'));
+        diag("QA contact       : " . $sel->get_value('qa_contact'));
+        diag("CC List          : " . join(', ', $sel->get_select_options('cc')));
+    }
+}
+
+sub set_flag {
+    my ($sel, $login, $curval, $newval, $prev_id) = @_;
+
+    # Retrieve flag id for the flag to be set
+    my $flag_id = $prev_id;
+    if (defined $login) {
+        my $flag_name = $sel->get_attribute("//table[\@id='flags']//input[\@value='$login']\@name");
+        $flag_name =~ /^requestee-(\d+)$/;
+        $flag_id = $1;
+    }
+
+    # Set new value for the flag (verifies current value)
+    $sel->select_ok("//select[\@id=\"flag-$flag_id\"]/option[\@value=\"$curval\" and \@selected]/..", "value=$newval", "Set flag ID $flag_id to $newval from $curval");
+
+   return $flag_id;
+}
diff --git a/xt/selenium/enter_new_bug.t b/xt/selenium/enter_new_bug.t
new file mode 100644 (file)
index 0000000..404d30f
--- /dev/null
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Very simple test script to test if bug creation with minimal data
+# passes successfully for different user privileges.
+#
+# More elaborate tests exist in other scripts. This doesn't mean this
+# one could not be improved a bit.
+
+my $bug_summary = "Bug created by Selenium";
+foreach my $user (qw(admin unprivileged canconfirm)) {
+    log_in($sel, $config, $user);
+    file_bug_in_product($sel, "TestProduct");
+    $sel->type_ok("short_desc", $bug_summary, "Enter bug summary");
+    $sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description");
+    create_bug($sel, $bug_summary);
+    logout($sel);
+}
diff --git a/xt/selenium/flags.t b/xt/selenium/flags.t
new file mode 100644 (file)
index 0000000..dd4a0ff
--- /dev/null
@@ -0,0 +1,441 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+# We have to upload files from the local computer. This requires
+# chrome privileges.
+my ($sel, $config) = get_selenium(CHROME_MODE);
+
+# First create a flag type for bugs.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Flags");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Administer Flag Types");
+$sel->click_ok("link=Create Flag Type for Bugs");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->type_ok("name", "SeleniumBugFlag1Test");
+$sel->type_ok("description", "bugflag1");
+$sel->select_ok("product", "label=TestProduct");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->remove_all_selections_ok("inclusion_to_remove");
+$sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
+$sel->click_ok("categoryAction-removeInclusion");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->select_ok("product", "label=QA-Selenium-TEST");
+$sel->click_ok("categoryAction-exclude");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->select_ok("product", "label=QA-Selenium-TEST");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+my @inclusion = $sel->get_select_options("inclusion_to_remove");
+ok(scalar @inclusion == 2, "The inclusion list contains 2 elements");
+ok(grep($_ eq "QA-Selenium-TEST:__Any__", @inclusion), "QA-Selenium-TEST:__Any__ is in the inclusion list");
+ok(grep($_ eq "TestProduct:__Any__", @inclusion), "TestProduct:__Any__ is in the inclusion list");
+my @exclusion = $sel->get_select_options("exclusion_to_remove");
+ok(scalar @exclusion == 1, "The exclusion list contains 1 element");
+ok($exclusion[0] eq "QA-Selenium-TEST:__Any__", "QA-Selenium-TEST:__Any__ is in the exclusion list");
+$sel->type_ok("sortkey", "900");
+$sel->value_is("cc_list", "");
+$sel->value_is("is_active", "on");
+$sel->value_is("is_requestable", "on");
+$sel->value_is("is_requesteeble", "on");
+$sel->value_is("is_multiplicable", "on");
+$sel->select_ok("grant_group", "label=admin");
+$sel->select_ok("request_group", "label=(no group)");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'SeleniumBugFlag1Test' Created");
+$sel->is_text_present_ok("The flag type SeleniumBugFlag1Test has been created.");
+my $flagtype_url = $sel->get_attribute('link=SeleniumBugFlag1Test@href');
+$flagtype_url =~ /id=(\d+)$/;
+my $flagtype1_id = $1;
+
+# Clone the flag type, but set the request group to 'editbugs' and the sortkey to 950.
+
+$sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$flagtype1_id']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs Based on SeleniumBugFlag1Test");
+$sel->type_ok("name", "SeleniumBugFlag2Test");
+$sel->type_ok("description", "bugflag2");
+@inclusion = $sel->get_select_options("inclusion_to_remove");
+ok(scalar @inclusion == 2, "The inclusion list contains 2 elements");
+ok(grep($_ eq "QA-Selenium-TEST:__Any__", @inclusion), "QA-Selenium-TEST:__Any__ is in the inclusion list");
+ok(grep($_ eq "TestProduct:__Any__", @inclusion), "TestProduct:__Any__ is in the inclusion list");
+@exclusion = $sel->get_select_options("exclusion_to_remove");
+ok(scalar @exclusion == 1, "The exclusion list contains 1 element");
+ok($exclusion[0] eq "QA-Selenium-TEST:__Any__", "QA-Selenium-TEST:__Any__ is in the exclusion list");
+$sel->type_ok("sortkey", "950");
+$sel->value_is("is_active", "on");
+$sel->value_is("is_requestable", "on");
+$sel->value_is("is_requesteeble", "on");
+$sel->value_is("is_multiplicable", "on");
+$sel->type_ok("cc_list", $config->{canconfirm_user_login});
+$sel->selected_label_is("grant_group", "admin");
+$sel->select_ok("request_group", "label=editbugs");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'SeleniumBugFlag2Test' Created");
+$sel->is_text_present_ok("The flag type SeleniumBugFlag2Test has been created.");
+$flagtype_url = $sel->get_attribute('link=SeleniumBugFlag2Test@href');
+$flagtype_url =~ /id=(\d+)$/;
+my $flagtype2_id = $1;
+
+# Clone the first flag type again, but with different attributes.
+
+$sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$flagtype1_id']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs Based on SeleniumBugFlag1Test");
+$sel->type_ok("name", "SeleniumBugFlag3Test");
+$sel->type_ok("description", "bugflag3");
+$sel->type_ok("sortkey", "980");
+$sel->value_is("is_active", "on");
+$sel->value_is("is_requestable", "on");
+$sel->uncheck_ok("is_requesteeble");
+$sel->uncheck_ok("is_multiplicable");
+$sel->value_is("cc_list", "");
+$sel->select_ok("grant_group", "label=(no group)");
+$sel->selected_label_is("request_group", "(no group)");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'SeleniumBugFlag3Test' Created");
+$sel->is_text_present_ok("The flag type SeleniumBugFlag3Test has been created.");
+$flagtype_url = $sel->get_attribute('link=SeleniumBugFlag3Test@href');
+$flagtype_url =~ /id=(\d+)$/;
+my $flagtype3_id = $1;
+
+# We now create a flag type for attachments.
+
+$sel->click_ok("link=Create Flag Type For Attachments");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments");
+$sel->type_ok("name", "SeleniumAttachmentFlag1Test");
+$sel->type_ok("description", "attachmentflag1");
+$sel->select_ok("product", "label=TestProduct");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments");
+$sel->remove_all_selections_ok("inclusion_to_remove");
+$sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
+$sel->click_ok("categoryAction-removeInclusion");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments");
+@inclusion = $sel->get_select_options("inclusion_to_remove");
+ok(scalar @inclusion == 1, "The inclusion list contains 1 element");
+ok($inclusion[0] eq "TestProduct:__Any__", "TestProduct:__Any__ is in the exclusion list");
+$sel->type_ok("sortkey", "700");
+$sel->value_is("cc_list", "");
+$sel->select_ok("grant_group", "label=editbugs");
+$sel->select_ok("request_group", "label=canconfirm");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'SeleniumAttachmentFlag1Test' Created");
+$sel->is_text_present_ok("The flag type SeleniumAttachmentFlag1Test has been created.");
+$flagtype_url = $sel->get_attribute('link=SeleniumAttachmentFlag1Test@href');
+$flagtype_url =~ /id=(\d+)$/;
+my $aflagtype1_id = $1;
+
+# Clone the flag type.
+
+$sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$aflagtype1_id']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments Based on SeleniumAttachmentFlag1Test");
+$sel->type_ok("name", "SeleniumAttachmentFlag2Test");
+$sel->type_ok("description", "attachmentflag2");
+@inclusion = $sel->get_select_options("inclusion_to_remove");
+ok(scalar @inclusion == 1, "The inclusion list contains 1 element");
+ok($inclusion[0] eq "TestProduct:__Any__", "TestProduct:__Any__ is in the exclusion list");
+$sel->type_ok("sortkey", "750");
+$sel->type_ok("cc_list", $config->{admin_user_login});
+$sel->uncheck_ok("is_multiplicable");
+$sel->select_ok("grant_group", "label=(no group)");
+$sel->select_ok("request_group", "label=(no group)");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'SeleniumAttachmentFlag2Test' Created");
+$sel->is_text_present_ok("The flag type SeleniumAttachmentFlag2Test has been created.");
+$flagtype_url = $sel->get_attribute('link=SeleniumAttachmentFlag2Test@href');
+$flagtype_url =~ /id=(\d+)$/;
+my $aflagtype2_id = $1;
+
+# Clone the flag type again, and set it as inactive.
+
+$sel->click_ok("//a[\@href='editflagtypes.cgi?action=copy&id=$aflagtype1_id']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments Based on SeleniumAttachmentFlag1Test");
+$sel->type_ok("name", "SeleniumAttachmentFlag3Test");
+$sel->type_ok("description", "attachmentflag3");
+$sel->type_ok("sortkey", "800");
+$sel->uncheck_ok("is_active");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'SeleniumAttachmentFlag3Test' Created");
+$sel->is_text_present_ok("The flag type SeleniumAttachmentFlag3Test has been created.");
+$flagtype_url = $sel->get_attribute('link=SeleniumAttachmentFlag3Test@href');
+$flagtype_url =~ /id=(\d+)$/;
+my $aflagtype3_id = $1;
+
+# All flag types have been created. Now "real" tests can start.
+
+file_bug_in_product($sel, 'TestProduct');
+my $bug_summary = "test flags";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "this bug is used by Selenium to test flags");
+# Restrict the bug to the Master group. That's important for subsequent tests!
+$sel->check_ok('//input[@name="groups" and @value="Master"]');
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# All 3 bug flag types must be available; we are in the TestProduct product.
+
+$sel->is_text_present_ok("SeleniumBugFlag1Test");
+# We specify //select or //input, just to be sure. This is not required, though.
+$sel->is_element_present_ok("//select[\@id='flag_type-$flagtype1_id']");
+$sel->is_element_present_ok("//input[\@id='requestee_type-$flagtype1_id']");
+# If fields are of the correct type above, we assume this is still true below.
+$sel->is_text_present_ok("SeleniumBugFlag2Test");
+$sel->is_element_present_ok("flag_type-$flagtype2_id");
+$sel->is_element_present_ok("requestee_type-$flagtype2_id");
+$sel->is_text_present_ok("SeleniumBugFlag3Test");
+$sel->is_element_present_ok("flag_type-$flagtype3_id");
+ok(!$sel->is_element_present("requestee_type-$flagtype3_id"), "SeleniumBugFlag3Test is not specifically requestable");
+
+# This is intentional to generate "flagmail". Some flags have a CC list
+# associated with them, some others don't. This is to catch crashes due to
+# the MTA.
+
+$sel->select_ok("flag_type-$flagtype1_id", "label=?");
+$sel->select_ok("flag_type-$flagtype2_id", "label=?");
+$sel->select_ok("flag_type-$flagtype3_id", "label=?");
+$sel->type_ok("comment", "Setting all 3 flags to ?");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+# We need to store the new flag IDs.
+
+$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumBugFlag1Test");
+my $flag1_1_id = $sel->get_attribute('//select[@title="bugflag1"]@id');
+$flag1_1_id =~ s/flag-//;
+$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumBugFlag2Test");
+my $flag2_1_id = $sel->get_attribute('//select[@title="bugflag2"]@id');
+$flag2_1_id =~ s/flag-//;
+$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumBugFlag3Test");
+my $flag3_1_id = $sel->get_attribute('//select[@title="bugflag3"]@id');
+$flag3_1_id =~ s/flag-//;
+
+$sel->is_text_present_ok("addl. SeleniumBugFlag1Test");
+$sel->is_text_present_ok("addl. SeleniumBugFlag2Test");
+ok(!$sel->is_text_present("addl. SeleniumBugFlag3Test"), "SeleniumBugFlag3Test is not multiplicable");
+$sel->select_ok("flag_type-$flagtype1_id", "label=+");
+$sel->select_ok("flag_type-$flagtype2_id", "label=-");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+# Now let's test requestees. SeleniumBugFlag2Test requires the requestee
+# to be in the editbugs group.
+
+$sel->select_ok("flag_type-$flagtype1_id", "label=?");
+$sel->type_ok("requestee_type-$flagtype1_id", $config->{admin_user_login});
+$sel->select_ok("flag_type-$flagtype2_id", "label=?");
+$sel->type_ok("requestee_type-$flagtype2_id", $config->{unprivileged_user_login});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Requestee Not Authorized");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->type_ok("requestee_type-$flagtype2_id", $config->{admin_user_login});
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+# Final tests for bug flags.
+
+$sel->select_ok("flag-$flag1_1_id", "value=X");
+$sel->select_ok("flag-$flag2_1_id", "label=+");
+$sel->select_ok("flag-$flag3_1_id", "label=-");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+# Now we test attachment flags.
+
+$sel->click_ok("link=Add an attachment");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create New Attachment for Bug #$bug1_id");
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "patch, v1");
+$sel->check_ok("ispatch");
+$sel->is_text_present_ok("SeleniumAttachmentFlag1Test");
+$sel->is_text_present_ok("SeleniumAttachmentFlag2Test");
+ok(!$sel->is_text_present("SeleniumAttachmentFlag3Test"), "Inactive SeleniumAttachmentFlag3Test flag type not displayed");
+
+# Let's generate some "flagmail", first with no requestee.
+
+$sel->select_ok("flag_type-$aflagtype1_id", "label=?");
+$sel->select_ok("flag_type-$aflagtype2_id", "label=?");
+$sel->type_ok("comment", "patch for testing purposes only");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
+
+# Store the flag ID.
+
+my $alink = $sel->get_attribute('//a[@title="patch, v1"]@href');
+$alink =~ /id=(\d+)/;
+my $attachment1_id = $1;
+
+# Now create another attachment, and set requestees.
+
+$sel->click_ok("link=Create Another Attachment to Bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create New Attachment for Bug #$bug1_id");
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "patch, v2");
+$sel->check_ok("ispatch");
+# Mark the previous attachment as obsolete.
+$sel->check_ok($attachment1_id);
+$sel->select_ok("flag_type-$aflagtype1_id", "label=?");
+$sel->type_ok("requestee_type-$aflagtype1_id", $config->{admin_user_login});
+$sel->select_ok("flag_type-$aflagtype2_id", "label=?");
+# The requestee is not in the Master group, and so he cannot view the bug.
+# He must be silently skipped from the requestee field.
+$sel->type_ok("requestee_type-$aflagtype2_id", $config->{unprivileged_user_login});
+$sel->type_ok("comment", "second patch, with requestee");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
+$alink = $sel->get_attribute('//a[@title="patch, v2"]@href');
+$alink =~ /id=(\d+)/;
+my $attachment2_id = $1;
+
+# Create a third attachment, but we now set the MIME type manually.
+
+$sel->click_ok("link=Create Another Attachment to Bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create New Attachment for Bug #$bug1_id");
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "patch, v3");
+$sel->click_ok("list");
+$sel->select_ok("contenttypeselection", "label=plain text (text/plain)");
+$sel->select_ok("flag_type-$aflagtype1_id", "label=+");
+$sel->type_ok("comment", "one +, the other one blank");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
+$alink = $sel->get_attribute('//a[@title="patch, v3"]@href');
+$alink =~ /id=(\d+)/;
+my $attachment3_id = $1;
+
+# Display the bug and check flags are correctly set.
+
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumAttachmentFlag1Test? ($config->{admin_user_username})");
+$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumAttachmentFlag2Test?");
+$sel->is_text_present_ok("$config->{admin_user_username}: SeleniumAttachmentFlag1Test+");
+# We marked the first attachment as obsolete, so it should have no flag on it.
+$sel->is_text_present_ok("no flags");
+
+# Make the bug public and log out.
+
+$sel->uncheck_ok('//input[@name="groups" and @value="Master"]');
+edit_bug($sel, $bug1_id, $bug_summary);
+logout($sel);
+
+# As an unprivileged user, try to edit flags.
+
+log_in($sel, $config, 'unprivileged');
+go_to_bug($sel, $bug1_id);
+# No privs are required to clear this flag.
+$sel->select_ok("flag-$flag3_1_id", "value=X");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+# editbugs privs are required to clear this flag, so no other option
+# should be displayed besides the currently set "+".
+
+my @flag_states = $sel->get_select_options("flag-$flag2_1_id");
+ok(scalar(@flag_states) == 1 && $flag_states[0] eq '+', "Single flag state '+' available");
+
+# Powerless users cannot set the flag to +, but setting it to ? is allowed.
+
+@flag_states = $sel->get_select_options("flag_type-$flagtype1_id");
+ok(scalar @flag_states == 2, "Two flag states available");
+ok(grep($_ eq '?', @flag_states), "Flag state '?' available");
+
+# A powerless user cannot edit someone else's attachment flags.
+
+$sel->click_ok("//a[\@href='attachment.cgi?id=$attachment2_id&action=edit']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/Attachment $attachment2_id Details for Bug $bug1_id/);
+ok($sel->is_element_present('//select[@title="attachmentflag2" and @disabled]'),
+   "Attachment flags are not editable by a powerless user");
+
+# Add an attachment and set flags on it.
+
+$sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug1_id')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id/);
+$sel->click_ok("link=Add an attachment");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create New Attachment for Bug #$bug1_id");
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "patch, v4");
+$sel->value_is("ispatch", "off");
+$sel->value_is("autodetect", "on");
+
+# canconfirm/editbugs privs are required to edit this flag.
+
+ok(!$sel->is_element_present("flag_type-$aflagtype1_id"), "Flag type 'SeleniumAttachmentFlag1Test' not displayed to powerless users");
+
+# No privs are required to edit this flag.
+
+$sel->select_ok("flag_type-$aflagtype2_id", "label=+");
+$sel->type_ok("comment", "granting again");
+edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "create"});
+$sel->is_text_present_ok("$config->{unprivileged_user_username}: SeleniumAttachmentFlag2Test+");
+logout($sel);
+
+# Final tests as an admin. He has editbugs privs, so he can edit
+# someone else's patch.
+
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("//a[\@href='attachment.cgi?id=${attachment3_id}&action=edit']");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/Attachment $attachment3_id Details for Bug $bug1_id/);
+$sel->select_ok('//select[@title="attachmentflag1"]', "label=+");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "update"});
+
+# It's time to delete all created flag types.
+
+go_to_admin($sel);
+$sel->click_ok("link=Flags");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Administer Flag Types");
+
+foreach my $flagtype ([$flagtype1_id, "SeleniumBugFlag1Test"], [$flagtype2_id, "SeleniumBugFlag2Test"],
+                      [$flagtype3_id, "SeleniumBugFlag3Test"], [$aflagtype1_id, "SeleniumAttachmentFlag1Test"],
+                      [$aflagtype2_id, "SeleniumAttachmentFlag2Test"], [$aflagtype3_id, "SeleniumAttachmentFlag3Test"])
+{
+    my $flag_id = $flagtype->[0];
+    my $flag_name = $flagtype->[1];
+    $sel->click_ok("//a[\@href='editflagtypes.cgi?action=confirmdelete&id=$flag_id']");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Confirm Deletion of Flag Type '$flag_name'");
+    $sel->click_ok("link=Yes, delete");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Flag Type '$flag_name' Deleted");
+    my $msg = trim($sel->get_text("message"));
+    ok($msg eq "The flag type $flag_name has been deleted.", "Flag type $flag_name deleted");
+}
+logout($sel);
diff --git a/xt/selenium/flags2.t b/xt/selenium/flags2.t
new file mode 100644 (file)
index 0000000..9b92161
--- /dev/null
@@ -0,0 +1,308 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+################################################################
+# 2nd script about flags. This one is focused on flag behavior #
+# when moving a bug from one product/component to another one. #
+################################################################
+
+# We have to upload files from the local computer. This requires
+# chrome privileges.
+my ($sel, $config) = get_selenium(CHROME_MODE);
+
+# Start by creating a flag type for bugs.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Flags");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Administer Flag Types");
+$sel->click_ok("link=Create Flag Type for Bugs");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->type_ok("name", "selenium");
+$sel->type_ok("description", "Available in TestProduct and Another Product/c1");
+$sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
+$sel->click_ok("categoryAction-removeInclusion");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->select_ok("product", "label=TestProduct");
+$sel->selected_label_is("component", "__Any__");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->select_ok("product", "label=Another Product");
+$sel->select_ok("component", "label=c1");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+
+# This flag type must have a higher sortkey than the one we will create later.
+# The reason is that link=selenium will catch the first link with this name in
+# the UI, so when the second flag type with this name is created, we have to
+# catch it, not this one (which will be unique for now, so no worry to find it).
+
+$sel->type_ok("sortkey", 100);
+$sel->value_is("is_active", "on");
+$sel->value_is("is_requestable", "on");
+$sel->click_ok("is_multiplicable");
+$sel->value_is("is_multiplicable", "off");
+$sel->select_ok("grant_group", "label=editbugs");
+$sel->select_ok("request_group", "label=canconfirm");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'selenium' Created");
+$sel->is_text_present_ok("The flag type selenium has been created.");
+
+# Store the flag type ID.
+
+$sel->click_ok("link=selenium");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $flag_url = $sel->get_location();
+$flag_url =~ /id=(\d+)/;
+my $flagtype1_id = $1;
+
+# Now create a flag type for attachments in 'Another Product'.
+
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->click_ok("link=Create Flag Type For Attachments");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments");
+$sel->type_ok("name", "selenium_review");
+$sel->type_ok("description", "Review flag used by Selenium");
+$sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
+$sel->click_ok("categoryAction-removeInclusion");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments");
+$sel->select_ok("product", "label=Another Product");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments");
+$sel->type_ok("sortkey", 100);
+$sel->value_is("is_active", "on");
+$sel->value_is("is_requestable", "on");
+$sel->click_ok("is_multiplicable");
+$sel->value_is("is_multiplicable", "off");
+$sel->selected_label_is("grant_group", "(no group)");
+$sel->selected_label_is("request_group", "(no group)");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'selenium_review' Created");
+$sel->is_text_present_ok("The flag type selenium_review has been created.");
+
+# Store the flag type ID.
+
+$sel->click_ok("link=selenium_review");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$flag_url = $sel->get_location();
+$flag_url =~ /id=(\d+)/;
+my $aflagtype1_id = $1;
+
+# Create a 2nd flag type for attachments, with the same name
+# as the 1st one, but now *excluded* from 'Another Product'.
+
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->click_ok("link=Create Flag Type For Attachments");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->type_ok("name", "selenium_review");
+$sel->type_ok("description", "Another review flag used by Selenium");
+$sel->select_ok("product", "label=Another Product");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Attachments");
+$sel->type_ok("sortkey", 50);
+$sel->value_is("is_active", "on");
+$sel->value_is("is_requestable", "on");
+$sel->value_is("is_multiplicable", "on");
+$sel->select_ok("grant_group", "label=editbugs");
+$sel->select_ok("request_group", "label=canconfirm");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'selenium_review' Created");
+
+# Store the flag type ID.
+
+$sel->click_ok("link=selenium_review");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$flag_url = $sel->get_location();
+$flag_url =~ /id=(\d+)/;
+my $aflagtype2_id = $1;
+
+# We are done with the admin tasks. Now play with flags in bugs.
+
+file_bug_in_product($sel, 'TestProduct');
+$sel->select_ok("flag_type-$flagtype1_id", "label=+");
+my $bug_summary = "The selenium flag should be kept on product change";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "pom");
+$sel->click_ok('//input[@value="Add an attachment"]');
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "small patch");
+$sel->click_ok("ispatch");
+$sel->value_is("ispatch", "on");
+ok(!$sel->is_element_present("flag_type-$aflagtype1_id"), "Flag type $aflagtype1_id not available in TestProduct");
+$sel->select_ok("flag_type-$aflagtype2_id", "label=-");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+$sel->is_text_present_ok("$config->{admin_user_username}: selenium");
+my $flag1_id = $sel->get_attribute('//select[@title="Available in TestProduct and Another Product/c1"]@id');
+$flag1_id =~ s/flag-//;
+$sel->selected_label_is("flag-$flag1_id", "+");
+$sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
+
+# Now move the bug into the 'Another Product' product.
+# Both the bug and attachment flags should survive.
+
+$sel->select_ok("product", "label=Another Product");
+$sel->type_ok("comment", "Moving to Another Product / c1. The flag should be preserved.");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Verify New Product Details...");
+$sel->select_ok("component", "label=c1");
+edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "change_product"});
+$sel->selected_label_is("flag-$flag1_id", "+");
+$sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
+
+# Now moving the bug into the c2 component. The bug flag
+# won't survive, but the attachment flag should.
+
+$sel->type_ok("comment", "Moving to c2. The selenium flag will be deleted.");
+$sel->select_ok("component", "label=c2");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+ok(!$sel->is_element_present("flag-$flag1_id"), "The selenium bug flag didn't survive");
+ok(!$sel->is_element_present("flag_type-$flagtype1_id"), "The selenium flag type doesn't exist");
+$sel->is_text_present_ok("$config->{admin_user_username}: selenium_review-");
+
+# File a bug in 'Another Product / c2' and assign it
+# to a powerless user, so that he can move it later.
+
+file_bug_in_product($sel, 'Another Product');
+$sel->select_ok("component", "label=c2");
+$sel->type_ok("assigned_to", $config->{unprivileged_user_login});
+ok(!$sel->is_editable("flag_type-$flagtype1_id"), "The selenium bug flag type is displayed but not selectable");
+$sel->select_ok("component", "label=c1");
+$sel->is_editable_ok("flag_type-$flagtype1_id", "The selenium bug flag type is not selectable");
+$sel->select_ok("flag_type-$flagtype1_id", "label=?");
+my $bug_summary2 = "Create a new selenium flag for c2";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", ".");
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+$sel->is_text_present_ok("$config->{admin_user_username}: selenium");
+my $flag2_id = $sel->get_attribute('//select[@title="Available in TestProduct and Another Product/c1"]@id');
+$flag2_id =~ s/flag-//;
+$sel->selected_label_is("flag-$flag2_id", '?');
+
+# Create a 2nd bug flag type, again named 'selenium', but now
+# for the 'Another Product / c2' component only.
+
+go_to_admin($sel);
+$sel->click_ok("link=Flags");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Administer Flag Types");
+$sel->click_ok("link=Create Flag Type for Bugs");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->type_ok("name", "selenium");
+$sel->type_ok("description", "Another flag with the selenium name");
+$sel->add_selection_ok("inclusion_to_remove", "label=__Any__:__Any__");
+$sel->click_ok("categoryAction-removeInclusion");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->select_ok("product", "label=Another Product");
+$sel->select_ok("component", "label=c2");
+$sel->click_ok("categoryAction-include");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create Flag Type for Bugs");
+$sel->type_ok("sortkey", 50);
+$sel->value_is("is_active", "on");
+$sel->value_is("is_requestable", "on");
+$sel->value_is("is_multiplicable", "on");
+$sel->selected_label_is("grant_group", "(no group)");
+$sel->selected_label_is("request_group", "(no group)");
+$sel->click_ok("save");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Flag Type 'selenium' Created");
+
+# Store the flag type ID.
+
+$sel->click_ok("link=selenium");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$flag_url = $sel->get_location();
+$flag_url =~ /id=(\d+)/;
+my $flagtype2_id = $1;
+
+# Now move the bug from c1 into c2. The bug flag should survive.
+
+go_to_bug($sel, $bug2_id);
+$sel->select_ok("component", "label=c2");
+ok(!$sel->is_checked("set_default_assignee"), "Moving the bug into another component must not change the assignee");
+$sel->type_ok("comment", "The selenium flag should be preserved.");
+edit_bug_and_return($sel, $bug2_id, $bug_summary2);
+$sel->selected_label_is("flag-$flag2_id", '?');
+ok(!$sel->is_element_present("flag_type-$flagtype1_id"), "Flag type not available in component c2");
+$sel->is_element_present_ok("flag_type-$flagtype2_id");
+logout($sel);
+
+# Powerless users can edit the 'selenium' flag being in c2.
+
+log_in($sel, $config, 'unprivileged');
+go_to_bug($sel, $bug2_id);
+$sel->select_ok("flag-$flag2_id", "label=+");
+edit_bug_and_return($sel, $bug2_id, $bug_summary2);
+$sel->selected_label_is("flag-$flag2_id", "+");
+
+# But moving the bug into TestProduct will delete the flag
+# as the flag setter is not in the editbugs group.
+
+$sel->select_ok("product", "label=TestProduct");
+$sel->type_ok("comment", "selenium flag will be lost. I don't have editbugs privs.");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Verify New Product Details...");
+edit_bug_and_return($sel, $bug2_id, $bug_summary2, {id => "change_product"});
+ok(!$sel->is_element_present("flag-$flag2_id"), "Flag $flag2_id deleted");
+ok(!$sel->is_element_present("flag_type-$flagtype1_id"), "Flag type 'selenium' not displayed to powerless users");
+ok(!$sel->is_element_present("flag_type-$flagtype2_id"), "Flag type not available in component c1");
+logout($sel);
+
+# Time to delete created flag types.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Flags");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Administer Flag Types");
+
+foreach my $flagtype ([$flagtype1_id, "selenium"], [$flagtype2_id, "selenium"],
+                      [$aflagtype1_id, "selenium_review"], [$aflagtype2_id, "selenium_review"])
+{
+    my $flag_id = $flagtype->[0];
+    my $flag_name = $flagtype->[1];
+    $sel->click_ok("//a[\@href='editflagtypes.cgi?action=confirmdelete&id=$flag_id']");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Confirm Deletion of Flag Type '$flag_name'");
+    $sel->click_ok("link=Yes, delete");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Flag Type '$flag_name' Deleted");
+    my $msg = trim($sel->get_text("message"));
+    ok($msg eq "The flag type $flag_name has been deleted.", "Flag type $flag_name deleted");
+}
+logout($sel);
diff --git a/xt/selenium/groups.t b/xt/selenium/groups.t
new file mode 100644 (file)
index 0000000..b755caf
--- /dev/null
@@ -0,0 +1,378 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Add the new Selenium-test group.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Add Group");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Add group");
+$sel->type_ok("name", "Selenium-test");
+$sel->type_ok("desc", "Test group for Selenium");
+$sel->check_ok("isactive");
+$sel->uncheck_ok("insertnew");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("New Group Created");
+my $group_id = $sel->get_value("group_id");
+
+# Mark the Selenium-test group as Shown/Mandatory for TestProduct.
+
+edit_product($sel, "TestProduct");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Group Controls for TestProduct");
+$sel->is_text_present_ok("Selenium-test");
+$sel->select_ok("membercontrol_${group_id}", "label=Shown");
+$sel->select_ok("othercontrol_${group_id}", "label=Mandatory");
+$sel->click_ok("submit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Update group access controls for TestProduct");
+
+# File a new bug in the TestProduct product, and restrict it to the bug group.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->is_text_present_ok("Test group for Selenium");
+$sel->value_is("group_${group_id}", "off"); # Must be OFF (else that's a bug)
+$sel->check_ok("group_${group_id}");
+my $bug_summary = "bug restricted to the Selenium group";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "should be invisible");
+$sel->selected_label_is("component", "TestComponent");
+my $bug1_id = create_bug($sel, $bug_summary);
+$sel->is_text_present_ok("Test group for Selenium");
+$sel->value_is("group_${group_id}", "on"); # Must be ON
+
+# Look for this new bug and add it to the new "Selenium bugs" saved search.
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections_ok("product");
+$sel->add_selection_ok("product", "TestProduct");
+$sel->remove_all_selections("bug_status");
+$sel->add_selection_ok("bug_status", "UNCONFIRMED");
+$sel->add_selection_ok("bug_status", "CONFIRMED");
+$sel->select_ok("f1", "Group");
+$sel->select_ok("o1", "is equal to");
+$sel->type_ok("v1", "Selenium-test");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("One bug found");
+$sel->is_text_present_ok("bug restricted to the Selenium group");
+$sel->type_ok("save_newqueryname", "Selenium bugs");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("OK, you have a new search named Selenium bugs");
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_text_present_ok("One bug found");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+
+# No longer use Selenium-test as a bug group.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Selenium-test");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->value_is("isactive", "on");
+$sel->click_ok("isactive");
+$sel->click_ok('//input[@value="Update Group"]');
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->is_text_present_ok("The group will no longer be used for bugs");
+
+# File another new bug, now visible as the bug group is disabled.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->selected_label_is("component", "TestComponent");
+my $bug_summary2 = "bug restricted to the Selenium group";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "should be *visible* when created (the group is disabled)");
+ok(!$sel->is_text_present("Test group for Selenium"), "Selenium-test group unavailable");
+ok(!$sel->is_element_present("group_${group_id}"), "Selenium-test checkbox not present");
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+# Make sure the new bug doesn't appear in the "Selenium bugs" saved search.
+
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_text_present_ok("One bug found");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+ok(!$sel->is_element_present("b$bug2_id"), "Bug $bug2_id NOT restricted to the bug group");
+
+# Re-enable the Selenium-test group as bug group. This doesn't affect
+# already filed bugs as this group is not mandatory.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Selenium-test");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->value_is("isactive", "off");
+$sel->click_ok("isactive");
+$sel->title_is("Change Group: Selenium-test");
+$sel->click_ok('//input[@value="Update Group"]');
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->is_text_present_ok("The group will now be used for bugs");
+
+# Make sure the second filed bug has not been added to the bug group.
+
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_text_present_ok("One bug found");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+ok(!$sel->is_element_present("b$bug2_id"), "Bug $bug2_id NOT restricted to the bug group");
+
+# Make the Selenium-test group mandatory for TestProduct.
+
+edit_product($sel, "TestProduct");
+$sel->is_text_present_ok("Selenium-test:Shown/Mandatory");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->select_ok("membercontrol_${group_id}", "Mandatory");
+$sel->click_ok("submit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Confirm Group Control Change for product 'TestProduct'");
+$sel->is_text_present_ok("this group is mandatory and will be added");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Update group access controls for TestProduct");
+$sel->is_text_present_ok('regexp:Adding bugs to group \'Selenium-test\' which is now mandatory for this product');
+
+# All bugs being in TestProduct must now be restricted to the bug group.
+
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
+
+# File a new bug, which must automatically be restricted to the bug group.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->selected_label_is("component", "TestComponent");
+my $bug_summary3 = "Selenium-test group mandatory";
+$sel->type_ok("short_desc", $bug_summary3);
+$sel->type_ok("comment", "group enabled");
+ok(!$sel->is_text_present("Test group for Selenium"), "Selenium-test group not available");
+ok(!$sel->is_element_present("group_${group_id}"), "Selenium-test checkbox not present (mandatory group)");
+my $bug3_id = create_bug($sel, $bug_summary3);
+
+# Make sure all three bugs are listed as being restricted to the bug group.
+
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug3_id", undef, "Bug $bug3_id restricted to the bug group");
+
+# Turn off the Selenium-test group again.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Selenium-test");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->value_is("isactive", "on");
+$sel->click_ok("isactive");
+$sel->click_ok("//input[\@value='Update Group']");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->is_text_present_ok("The group will no longer be used for bugs");
+
+# File a bug again. It should not be added to the bug group as this one is disabled.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->selected_label_is("component", "TestComponent");
+my $bug_summary4 = "bug restricted to the Selenium-test group";
+$sel->type_ok("short_desc", $bug_summary4);
+$sel->type_ok("comment", "group disabled");
+ok(!$sel->is_text_present("Test group for Selenium"), "Selenium-test group not available");
+ok(!$sel->is_element_present("group_${group_id}"), "Selenium-test checkbox not present");
+my $bug4_id = create_bug($sel, $bug_summary4);
+
+# The last bug must not be in the list.
+
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug3_id", undef, "Bug $bug3_id restricted to the bug group");
+ok(!$sel->is_element_present("b$bug4_id"), "Bug $bug4_id NOT restricted to the bug group");
+
+# Re-enable the mandatory group. All bugs should be restricted to this bug group automatically.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Selenium-test");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->value_is("isactive", "off");
+$sel->click_ok("isactive");
+$sel->click_ok("//input[\@value='Update Group']");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->is_text_present_ok("The group will now be used for bugs");
+
+# Make sure all bugs are restricted to the bug group.
+
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug2_id", undef, "Bug $bug2_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug3_id", undef, "Bug $bug3_id restricted to the bug group");
+$sel->is_element_present_ok("b$bug4_id", undef, "Bug $bug4_id restricted to the bug group");
+
+# Try to remove the Selenium-test group from TestProduct, but DON'T do it!
+# We just want to make sure a warning is displayed about this removal.
+
+edit_product($sel, "TestProduct");
+$sel->is_text_present_ok("Selenium-test:Mandatory/Mandatory");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Group Controls for TestProduct");
+$sel->is_text_present_ok("Selenium-test");
+$sel->select_ok("membercontrol_${group_id}", "NA");
+$sel->select_ok("othercontrol_${group_id}", "NA");
+$sel->click_ok("submit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Confirm Group Control Change for product 'TestProduct'");
+$sel->is_text_present_ok("this group is no longer applicable and will be removed");
+
+# Make sure that renaming a group which is used as a special group
+# (such as insidergroup or querysharegroup) is correctly propagated
+# and that you cannot delete this group.
+
+set_parameters($sel, { "Group Security" => {"querysharegroup" => {type => "select", value => "Selenium-test"}} });
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Selenium-test");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->type_ok("name", "X-Selenium-Y");
+$sel->click_ok("update-group");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: X-Selenium-Y");
+$sel->is_text_present_ok("The name was changed to 'X-Selenium-Y'");
+
+go_to_admin($sel);
+$sel->click_ok("link=Parameters");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Configuration: Required Settings");
+$sel->click_ok("link=Group Security");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Configuration: Group Security");
+$sel->value_is("querysharegroup", "X-Selenium-Y");
+
+# There is no UI to delete this group, so we have to type the URL directly.
+
+$sel->open_ok("/$config->{bugzilla_installation}/editgroups.cgi?action=del&group=$group_id");
+$sel->title_is("Group not deletable");
+$sel->is_text_present_ok("The group 'X-Selenium-Y' is used by the 'querysharegroup' parameter");
+
+$sel->open_ok("/$config->{bugzilla_installation}/editgroups.cgi?action=delete&group=$group_id");
+$sel->title_is("Suspicious Action");
+$sel->is_text_present_ok("you have no valid token for the delete_group action while processing the 'editgroups.cgi' script");
+$sel->click_ok("confirm");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Group not deletable");
+$sel->is_text_present_ok("The group 'X-Selenium-Y' is used by the 'querysharegroup' parameter");
+
+set_parameters($sel, { "Group Security" => {"querysharegroup" => {type => "select", value => ""}} });
+
+# Revert the group name change to not mess with the subsequent tests
+# which expect to see 'Selenium-test'.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=X-Selenium-Y");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: X-Selenium-Y");
+$sel->type_ok("name", "Selenium-test");
+$sel->click_ok("update-group");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: Selenium-test");
+$sel->is_text_present_ok("The name was changed to 'Selenium-test'");
+
+# Delete the Selenium-test group.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("//a[\@href='editgroups.cgi?action=del&group=${group_id}']");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^Delete group/);
+$sel->is_text_present_ok("Do you really want to delete this group?");
+$sel->is_element_present_ok("removebugs");
+$sel->value_is("removebugs", "off");
+$sel->is_text_present_ok("Remove all bugs from this group restriction for me");
+$sel->is_element_present_ok("unbind");
+$sel->value_is("unbind", "off");
+$sel->is_text_present_ok("remove these controls");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Cannot Delete Group");
+my $error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /^The Selenium-test group cannot be deleted/, "Group is in use - not deletable");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->check("removebugs");
+$sel->check("unbind");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Group Deleted");
+$sel->is_text_present_ok("The group Selenium-test has been deleted.");
+
+# No more bugs listed in the saved search as the bug group is gone.
+
+$sel->click_ok("link=Selenium bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: Selenium bugs");
+$sel->is_text_present_ok("Zarro Boogs found");
+$sel->click_ok("forget_search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Search is gone");
+$sel->is_text_present_ok("OK, the Selenium bugs search is gone.");
+logout($sel);
diff --git a/xt/selenium/keywords.t b/xt/selenium/keywords.t
new file mode 100644 (file)
index 0000000..16ecf90
--- /dev/null
@@ -0,0 +1,181 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Create keywords. Do some cleanup first if necessary.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Keywords");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Select keyword");
+
+# If keywords already exist, delete them to not disturb the test.
+
+my $page = $sel->get_body_text();
+my @keywords = $page =~ m/(key-selenium-\w+)/gi;
+
+foreach my $keyword (@keywords) {
+    my $url = $sel->get_attribute("link=$keyword\@href");
+    $url =~ s/action=edit/action=del/;
+    $sel->click_ok("//a[\@href='$url']");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Delete Keyword");
+    $sel->click_ok("delete");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Keyword Deleted");
+}
+
+# Now let's create our first keyword.
+
+go_to_admin($sel);
+$sel->click_ok("link=Keywords");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Select keyword");
+$sel->click_ok("link=Add a new keyword");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Add keyword");
+$sel->type_ok("name", "key-selenium-kone");
+$sel->type_ok("description", "Hopefully an ice cream");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("New Keyword Created");
+
+# Try create the same keyword, to check validators.
+
+$sel->click_ok("link=Add a new keyword");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Add keyword");
+$sel->type_ok("name", "key-selenium-kone");
+$sel->type_ok("description", "FIX ME!");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Keyword Already Exists");
+my $error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg eq 'A keyword with the name key-selenium-kone already exists.', 'Already created keyword');
+$sel->go_back_ok();
+$sel->wait_for_page_to_load(WAIT_TIME);
+
+# Create a second keyword.
+
+$sel->type_ok("name", "key-selenium-ktwo");
+$sel->type_ok("description", "FIX ME!");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("New Keyword Created");
+
+# Again test validators.
+
+$sel->click_ok("link=key-selenium-ktwo");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit keyword");
+$sel->type_ok("name", "key-selenium-kone");
+$sel->type_ok("description", "the second keyword");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Keyword Already Exists");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg eq 'A keyword with the name key-selenium-kone already exists.', 'Already created keyword');
+$sel->go_back_ok();
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit keyword");
+$sel->type_ok("name", "key-selenium-ktwo");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Keyword Updated");
+
+# Add keywords to bugs
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "TestComponent");
+$sel->type_ok("keywords", "key-selenium-kone");
+my $bug_summary = "It's a beautiful day";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "This bug is to test keywords");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "TestComponent");
+$sel->type_ok("keywords", "key-selenium-kone, key-selenium-ktwo");
+my $bug_summary2 = "Radio gaga";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "This bug is also to test keywords, like bug $bug1_id");
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+# Now make sure these bugs correctly appear in buglists.
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections("product");
+$sel->remove_all_selections("bug_status");
+$sel->type_ok("keywords", "key-selenium-kone");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("2 bugs found");
+
+$sel->click_ok("link=Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Search for bugs");
+$sel->remove_all_selections("product");
+$sel->remove_all_selections("bug_status");
+# Try with a different case than the one in the DB.
+$sel->type_ok("keywords", "key-selenium-ktWO");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("One bug found");
+
+$sel->click_ok("link=Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Search for bugs");
+$sel->remove_all_selections("product");
+$sel->remove_all_selections("bug_status");
+# Substrings also work for keywords.
+$sel->type_ok("keywords", "selenium");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("2 bugs found");
+
+# Make sure describekeywords.cgi works as expected.
+
+$sel->click_ok("link=$bug_summary");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->click_ok("link=Keywords:");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bugzilla Keyword Descriptions");
+$sel->is_text_present_ok("key-selenium-kone");
+$sel->is_text_present_ok("Hopefully an ice cream");
+$sel->is_text_present_ok("key-selenium-ktwo");
+$sel->is_text_present_ok("the second keyword");
+$sel->click_ok('//a[@href="buglist.cgi?keywords=key-selenium-kone"]');
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_element_present_ok("link=$bug1_id");
+$sel->is_element_present_ok("link=$bug2_id");
+$sel->is_text_present_ok("2 bugs found");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->click_ok('//a[@href="buglist.cgi?keywords=key-selenium-ktwo"]');
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_element_present_ok("link=$bug2_id");
+$sel->is_text_present_ok("One bug found");
+logout($sel);
diff --git a/xt/selenium/login.t b/xt/selenium/login.t
new file mode 100644 (file)
index 0000000..b41d9a2
--- /dev/null
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# FIXME - At some point, this trivial script should be merged with test_create_user_accounts.t.
+#       Either that or we should improve this script a lot.
+
+# Try to log in to Bugzilla using an invalid account. To be sure that the login form
+# is triggered, we try to file a new bug.
+
+go_to_home($sel, $config);
+$sel->click_ok("link=New");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Log in to Bugzilla");
+# The login and password are hardcoded here, because this account doesn't exist.
+$sel->type_ok("Bugzilla_login", 'guest@foo.com');
+$sel->type_ok("Bugzilla_password", 'foo-bar-baz');
+$sel->click_ok("log_in");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Invalid Login Or Password");
+$sel->is_text_present_ok("The login or password you entered is not valid.");
diff --git a/xt/selenium/milestones.t b/xt/selenium/milestones.t
new file mode 100644 (file)
index 0000000..35991fb
--- /dev/null
@@ -0,0 +1,149 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# 1st step: turn on usetargetmilestone and letsubmitterchoosemilestone.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, {'Bug Fields'          => {'usetargetmilestone-on'          => undef},
+                      'Bug Change Policies' => {'letsubmitterchoosemilestone-on' => undef},
+                     }
+              );
+
+# 2nd step: Add the milestone "2.0" (with sortkey = 10) to the TestProduct product.
+
+edit_product($sel, "TestProduct");
+$sel->click_ok("link=Edit milestones:", undef, "Go to the Edit milestones page");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'", "Display milestones");
+$sel->click_ok("link=Add", undef, "Go add a new milestone");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Add Milestone to Product 'TestProduct'", "Enter new milestone");
+$sel->type_ok("milestone", "2.0", "Set its name to 2.0");
+$sel->type_ok("sortkey", "10", "Set its sortkey to 10");
+$sel->click_ok("create", undef, "Submit data");
+$sel->wait_for_page_to_load(WAIT_TIME);
+# If the milestone already exists, that's not a big deal. So no special action
+# is required in this case.
+$sel->title_is("Milestone Created", "Milestone Created");
+
+# 3rd step: file a new bug, leaving the milestone alone (should fall back to the default one).
+
+file_bug_in_product($sel, "TestProduct");
+$sel->selected_label_is("component", "TestComponent", "Component already selected (no other component defined)");
+$sel->selected_label_is("target_milestone", "---", "Default milestone selected");
+$sel->selected_label_is("version", "unspecified", "Version already selected (no other version defined)");
+my $bug_summary = "Target Milestone left to default";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Created by Selenium to test 'musthavemilestoneonaccept'");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# 4th step: edit the bug
+
+go_to_bug($sel, $bug1_id);
+$sel->select_ok("bug_status", "label=IN_PROGRESS", "Change bug status to IN_PROGRESS");
+$sel->select_ok("target_milestone", "label=2.0", "Select a non-default milestone");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# 5th step: create another bug.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("target_milestone", "label=2.0", "Set the milestone to 2.0");
+$sel->selected_label_is("component", "TestComponent", "Component already selected (no other component defined)");
+$sel->selected_label_is("version", "unspecified", "Version already selected (no other version defined)");
+my $bug_summary2 = "Target Milestone set to non-default";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "Created by Selenium to test milestone support");
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+# 6th step: edit the bug
+
+$sel->select_ok("bug_status", "label=IN_PROGRESS");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+# 7th step: test validation methods for milestones.
+
+go_to_admin($sel);
+$sel->click_ok("link=milestones");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit milestones for which product?");
+$sel->click_ok("link=TestProduct");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'");
+$sel->click_ok("link=2.0");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Milestone '2.0' of product 'TestProduct'");
+$sel->type_ok("milestone", "1.0");
+$sel->value_is("milestone", "1.0");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Milestone Updated");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Add Milestone to Product 'TestProduct'");
+$sel->type_ok("milestone", "1.5");
+$sel->value_is("milestone", "1.5");
+$sel->type_ok("sortkey", "99999999999999999");
+$sel->value_is("sortkey", "99999999999999999");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Invalid Milestone Sortkey");
+my $error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /^The sortkey '99999999999999999' is not in the range/, "Invalid sortkey");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->type_ok("sortkey", "-polu7A");
+$sel->value_is("sortkey", "-polu7A");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Invalid Milestone Sortkey");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /^The sortkey '-polu7A' is not in the range/, "Invalid sortkey");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->click_ok("link='TestProduct'");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'");
+$sel->click_ok("link=Delete");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Delete Milestone of Product 'TestProduct'");
+$sel->is_text_present_ok("When you delete this milestone", undef, "Warn the user about bugs being affected");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Milestone Deleted");
+
+# 8th step: make sure the (now deleted) milestone of the bug has fallen back to the default milestone.
+
+go_to_bug($sel, $bug1_id);
+$sel->is_text_present_ok('regexp:Target Milestone:\W+---', undef, "Milestone has fallen back to the default milestone");
+
+# 9th step: file another bug.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->selected_label_is("target_milestone", "---", "Default milestone selected");
+$sel->selected_label_is("component", "TestComponent");
+my $bug_summary3 = "Only one Target Milestone available";
+$sel->type_ok("short_desc", $bug_summary3);
+$sel->type_ok("comment", "Created by Selenium to test milestone support");
+my $bug3_id = create_bug($sel, $bug_summary3);
+
+$sel->select_ok("bug_status", "label=IN_PROGRESS");
+edit_bug($sel, $bug3_id, $bug_summary3);
+
+logout($sel);
diff --git a/xt/selenium/password_complexity.t b/xt/selenium/password_complexity.t
new file mode 100644 (file)
index 0000000..e29ef8b
--- /dev/null
@@ -0,0 +1,123 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+log_in($sel, $config, 'admin');
+
+set_parameters($sel, {"Administrative Policies" => {"allowuserdeletion-on" => undef},
+                      "User Authentication"     => {"createemailregexp" => {type => "text", value => '.*'},
+                                                    "emailsuffix" => {type => "text", value => ''}} });
+
+# Set the password complexity to MIXED LETTERS.
+# Password must contain at least one UPPER and one lowercase letter.
+my @invalid_mixed_letter = qw(lowercase UPPERCASE 1234567890 123lowercase
+                              123UPPERCASE !@%&^lower !@&^UPPER);
+
+check_passwords($sel, 'mixed_letters', \@invalid_mixed_letter, ['PaSSwOrd', '%9rT#j22S']);
+
+# Set the password complexity to LETTERS AND NUMBERS.
+# Passwords must contain at least one UPPER and one lower case letter and a number.
+my @invalid_letter_number = (@invalid_mixed_letter, qw(lowerUPPER 123!@%^$));
+
+check_passwords($sel, 'letters_numbers', \@invalid_letter_number, ['-UniCode6.3', 'UNO54sun']);
+
+# Set the password complexity to LETTERS, NUMBERS AND SPECIAL CHARACTERS.
+# Passwords must contain at least one letter, a number and a special character.
+my @invalid_letter_number_splchar = (qw(!@%^&~* lowerUPPER123), @invalid_letter_number);
+
+check_passwords($sel, 'letters_numbers_specialchars', \@invalid_letter_number_splchar, ['@gu731', 'HU%m70?']);
+
+# Set the password complexity to No Constraints.
+check_passwords($sel, 'no_constraints', ['12xY!', 'aaaaa'], ['aaaaaaaa', '>F12Xy?']);
+
+logout($sel);
+
+
+sub check_passwords {
+    my ($sel, $param, $invalid_passwords, $valid_passwords) = @_;
+
+    set_parameters($sel, { "User Authentication" => {"password_complexity" => {type => "select", value => $param}} });
+    my $new_user = 'selenium-' . random_string(10) . '@bugzilla.org';
+
+    go_to_admin($sel);
+    $sel->click_ok("link=Users");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is('Search users');
+    $sel->click_ok('link=add a new user');
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is('Add user');
+    $sel->type_ok('login', $new_user);
+
+    foreach my $password (@$invalid_passwords) {
+        $sel->type_ok('password', $password, 'Enter password');
+        $sel->click_ok('add');
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        if ($param eq 'no_constraints') {
+            $sel->title_is('Password Too Short');
+        }
+        else {
+            $sel->title_is('Password Fails Requirements');
+        }
+
+        my $error_msg = trim($sel->get_text("error_msg"));
+        if ($param eq 'mixed_letters') {
+            ok($error_msg =~ /UPPERCASE letter.*lowercase letter/,
+               "Mixed letter password fails requirement: $password");
+        }
+        elsif ($param eq 'letters_numbers') {
+            ok($error_msg =~ /UPPERCASE letter.*lowercase letter.*digit/,
+               "Letter & Number password fails requirement: $password");
+
+        }
+        elsif ($param eq 'letters_numbers_specialchars') {
+            ok($error_msg =~ /letter.*special character.*digit/,
+               "Letter, Number & Special Character password fails requirement: $password");
+        }
+        else {
+            ok($error_msg =~ /The password must be at least \d+ characters long/,
+               "Password Too Short: $password");
+        }
+        $sel->go_back_ok();
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    }
+
+    my $created = 0;
+
+    foreach my $password (@$valid_passwords) {
+        $sel->type_ok('password', $password, 'Enter password');
+        $sel->click_ok($created ? 'update' : 'add');
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_is($created ? "User $new_user updated" : "Edit user $new_user");
+        my $msg = trim($sel->get_text('message'));
+        if ($created++) {
+            ok($msg =~ /A new password has been set/, 'Account updated');
+        }
+        else {
+            ok($msg =~ /The user account $new_user has been created successfully/, 'Account created');
+        }
+    }
+
+    return unless $created;
+
+    $sel->click_ok('delete');
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Confirm deletion of user $new_user");
+    $sel->click_ok('delete');
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("User $new_user deleted");
+}
diff --git a/xt/selenium/private_attachments.t b/xt/selenium/private_attachments.t
new file mode 100644 (file)
index 0000000..4dacd26
--- /dev/null
@@ -0,0 +1,173 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+# We have to upload files from the local computer. This requires
+# chrome privileges.
+my ($sel, $config) = get_selenium(CHROME_MODE);
+
+# set the insidergroup parameter to the admin group, and make sure
+# we can view and delete attachments.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Group Security" => {"insidergroup" => {type => "select", value => "admin"}},
+                       "Attachments"    => {"allow_attachment_display-on" => undef,
+                                            "allow_attachment_deletion-on" => undef}
+                     });
+
+# First create a new bug with a private attachment.
+
+file_bug_in_product($sel, "TestProduct");
+my $bug_summary = "Some comments are private";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "and some attachments too, like this one.");
+$sel->check_ok("comment_is_private");
+$sel->click_ok('//input[@value="Add an attachment"]');
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "private attachment, v1");
+$sel->check_ok("ispatch");
+my $bug1_id = create_bug($sel, $bug_summary);
+$sel->is_text_present_ok("private attachment, v1 (");
+$sel->is_text_present_ok("and some attachments too, like this one.");
+$sel->is_checked_ok('//a[@id="comment_link_0"]/../..//div//input[@type="checkbox"]');
+
+# Now attach a public patch to the existing bug.
+
+$sel->click_ok("link=Add an attachment");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create New Attachment for Bug #$bug1_id");
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "public attachment, v2");
+$sel->check_ok("ispatch");
+# The existing attachment name must be displayed, to mark it as obsolete.
+$sel->is_text_present_ok("private attachment, v1");
+$sel->type_ok("comment", "this patch is public. Everyone can see it.");
+$sel->value_is("isprivate", "off");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
+
+# We need to store the attachment ID.
+
+$sel->is_text_present_ok("public attachment, v2");
+my $alink = $sel->get_attribute('//a[@title="public attachment, v2"]@href');
+$alink =~ /id=(\d+)/;
+my $attachment1_id = $1;
+$sel->is_text_present_ok("this patch is public. Everyone can see it.");
+ok(!$sel->is_checked('//a[@id="comment_link_1"]/../..//div//input[@type="checkbox"]'), "Public attachment is visible");
+logout($sel);
+
+# A logged out user cannot see the private attachment, only the public one.
+# Same for a user with no privs.
+
+foreach my $user ('', 'unprivileged') {
+    log_in($sel, $config, $user) if $user;
+    go_to_bug($sel, $bug1_id);
+    ok(!$sel->is_text_present("private attachment, v1"), "Private attachment not visible");
+    $sel->is_text_present_ok("public attachment, v2");
+    ok(!$sel->is_text_present("and some attachments too, like this one"), "Private comment not visible");
+    $sel->is_text_present_ok("this patch is public. Everyone can see it.");
+}
+
+# A powerless user can comment on attachments he doesn't own.
+
+$sel->click_ok('//a[@href="attachment.cgi?id=' . $attachment1_id . '&action=edit"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/Attachment $attachment1_id Details for Bug $bug1_id/);
+$sel->is_text_present_ok("created by admin");
+$sel->type_ok("comment", "This attachment is not mine.");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "update"});
+$sel->is_text_present_ok("This attachment is not mine");
+
+# Powerless users will always be able to view their own attachments, even
+# when those are marked private by a member of the insider group.
+
+$sel->click_ok("link=Add an attachment");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create New Attachment for Bug #$bug1_id");
+$sel->type_ok("data", $config->{attachment_file});
+$sel->check_ok("ispatch");
+# The user doesn't have editbugs privs.
+ok(!$sel->is_text_present("Check each existing attachment made obsolete by your new attachment"), "No attachments can be marked as obsolete");
+$sel->type_ok("description", "My patch, which I should see, always");
+$sel->type_ok("comment", "This is my patch!");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
+$sel->is_text_present_ok("My patch, which I should see, always (");
+$alink = $sel->get_attribute('//a[@title="My patch, which I should see, always"]@href');
+$alink =~ /id=(\d+)/;
+my $attachment2_id = $1;
+$sel->is_text_present_ok("This is my patch!");
+logout($sel);
+
+# Let the admin mark the powerless user's attachment as private.
+
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug1_id);
+$sel->click_ok('//a[@href="attachment.cgi?id=' . $attachment2_id . '&action=edit"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/Attachment $attachment2_id Details for Bug $bug1_id/);
+$sel->check_ok("isprivate");
+$sel->type_ok("comment", "Making the powerless user's patch private.");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "update"});
+$sel->is_text_present_ok("My patch, which I should see, always (");
+$sel->is_checked_ok('//a[@id="comment_link_4"]/../..//div//input[@type="checkbox"]');
+$sel->is_text_present_ok("Making the powerless user's patch private.");
+logout($sel);
+
+# A logged out user cannot see private attachments.
+
+go_to_bug($sel, $bug1_id);
+ok(!$sel->is_text_present("private attachment, v1"), "Private attachment not visible to logged out users");
+ok(!$sel->is_text_present("My patch, which I should see, always ("), "Private attachment not visible to logged out users");
+$sel->is_text_present_ok("This is my patch!");
+ok(!$sel->is_text_present("Making the powerless user's patch private"), "Private comment not visible to logged out users");
+
+# A powerless user can only see private attachments he owns.
+
+log_in($sel, $config, 'unprivileged');
+go_to_bug($sel, $bug1_id);
+$sel->is_text_present_ok("My patch, which I should see, always (");
+$sel->click_ok("link=My patch, which I should see, always");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+# No title displayed while viewing an attachment.
+$sel->title_is("");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+logout($sel);
+
+# Admins can delete attachments.
+
+log_in($sel, $config, 'admin');
+go_to_bug($sel, $bug1_id);
+$sel->click_ok('//a[@href="attachment.cgi?id=' . $attachment2_id . '&action=edit"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/Attachment $attachment2_id Details for Bug $bug1_id/);
+$sel->click_ok("link=Delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Attachment $attachment2_id of Bug $bug1_id");
+$sel->is_text_present_ok("Do you really want to delete this attachment?");
+$sel->type_ok("reason", "deleted by Selenium");
+edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "delete"});
+$sel->is_text_present_ok("deleted by Selenium");
+$sel->click_ok("link=attachment $attachment2_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Attachment Removed");
+$sel->is_text_present_ok("The attachment you are attempting to access has been removed");
+
+set_parameters($sel, {
+    "Group Security" => {"insidergroup" => { type => "select",
+                                             value => "QA-Selenium-TEST" }},
+});
+logout($sel);
diff --git a/xt/selenium/qa_contact.t b/xt/selenium/qa_contact.t
new file mode 100644 (file)
index 0000000..c548a71
--- /dev/null
@@ -0,0 +1,164 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# First make sure the 'My QA query' saved search is gone.
+
+log_in($sel, $config, 'admin');
+if ($sel->is_text_present("My QA query")) {
+    $sel->click_ok("link=My QA query");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Bug List: My QA query");
+    $sel->click_ok("forget_search");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Search is gone");
+    $sel->is_text_present_ok("OK, the My QA query search is gone.");
+}
+
+# Enable the QA contact field and file a new bug restricted to the 'Master' group
+# with a powerless user as the QA contact. He should only be able to access the
+# bug if the QA contact field is enabled, else he looses this privilege.
+
+set_parameters($sel, { "Bug Fields" => {"useqacontact-on" => undef} });
+file_bug_in_product($sel, 'TestProduct');
+$sel->type_ok("qa_contact", $config->{unprivileged_user_login}, "Set the powerless user as QA contact");
+my $bug_summary = "Test for QA contact";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "This is a test to check QA contact privs.");
+$sel->check_ok('//input[@name="groups" and @value="Master"]');
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# Create a saved search querying for all bugs with the powerless user
+# as QA contact.
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections_ok("product");
+$sel->add_selection_ok("product", "TestProduct");
+$sel->remove_all_selections("bug_status");
+$sel->select_ok("f1", "label=QA Contact");
+$sel->select_ok("o1", "label=is equal to");
+$sel->type_ok("v1", $config->{unprivileged_user_login}, "Look for the powerless user as QA contact");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id is on the list");
+$sel->is_text_present_ok("Test for QA contact");
+$sel->type_ok("save_newqueryname", "My QA query");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+my $text = trim($sel->get_text("message"));
+ok($text =~ /OK, you have a new search named My QA query/, "New saved search 'My QA query'");
+$sel->click_ok("link=My QA query");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: My QA query");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id is on the list");
+$sel->is_text_present_ok("Test for QA contact");
+
+# The saved search should still work, even with the QA contact field disabled.
+# ("work" doesn't mean you should still see all bugs, depending on your role
+# and privs!)
+
+set_parameters($sel, { "Bug Fields" => {"useqacontact-off" => undef} });
+$sel->click_ok("link=My QA query");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: My QA query");
+$sel->is_text_present_ok("One bug found");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id is on the list");
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+# The 'QA Contact' label must not be displayed.
+ok(!$sel->is_text_present("QA Contact"), "The QA Contact label is not present");
+logout($sel);
+
+# You cannot access the bug when being logged out, as it's restricted
+# to the Master group.
+
+$sel->type_ok("quicksearch_top", $bug1_id);
+$sel->click_ok("find_top");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug Access Denied");
+$sel->is_text_present_ok("You are not authorized to access bug");
+
+# You are still not allowed to access the bug when logged in as the
+# powerless user, as the QA contact field is disabled.
+# Don't use it log_in() as we want to follow this specific link.
+
+$sel->click_ok("link=log in to an account", undef, "Log in");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Log in to Bugzilla");
+$sel->is_text_present_ok("Bugzilla needs a legitimate login and password to continue");
+$sel->type_ok("Bugzilla_login", $config->{unprivileged_user_login}, "Enter login name");
+$sel->type_ok("Bugzilla_password", $config->{unprivileged_user_passwd}, "Enter password");
+$sel->click_ok("log_in", undef, "Submit credentials");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug Access Denied");
+$sel->is_text_present_ok("You are not authorized to access bug");
+logout($sel);
+
+# Re-enable the QA contact field.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields" => {"useqacontact-on" => undef} });
+logout($sel);
+
+# Log in as the powerless user. As the QA contact field is enabled again,
+# you can now access the restricted bug.
+
+log_in($sel, $config, 'unprivileged');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->select_ok("state_addselfcc", "value=never");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections_ok("product");
+$sel->add_selection_ok("product", "TestProduct");
+$sel->remove_all_selections_ok("bug_status");
+$sel->select_ok("f1", "label=QA Contact");
+$sel->select_ok("o1", "label=is equal to");
+$sel->type_ok("v1", $config->{unprivileged_user_login}, "Look for the powerless user as QA contact");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("One bug found");
+$sel->is_element_present_ok("b$bug1_id", undef, "Bug $bug1_id is on the list");
+$sel->is_text_present_ok("Test for QA contact");
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/$bug1_id /);
+$sel->click_ok("bz_qa_contact_edit_action");
+$sel->value_is("qa_contact", $config->{unprivileged_user_login}, "The powerless user is the current QA contact");
+$sel->check_ok("set_default_qa_contact");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# The user is no longer the QA contact, and he has no other role
+# with the bug. He can no longer see it.
+
+$sel->is_text_present_ok("(list of e-mails not available)");
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug Access Denied");
+logout($sel);
+
+$sel->stop();
diff --git a/xt/selenium/require_login.t b/xt/selenium/require_login.t
new file mode 100644 (file)
index 0000000..d661121
--- /dev/null
@@ -0,0 +1,83 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Turn on 'requirelogin'.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "User Authentication" => {"requirelogin-on" => undef} });
+logout($sel);
+
+# We try to access each page. None of the ones listed below should
+# let you view it without being logged in.
+
+my @pages = qw(admin attachment buglist chart colchange describecomponents
+               describekeywords duplicates editclassifications editcomponents
+               editfields editflagtypes editgroups editkeywords editmilestones
+               editparams editproducts editsettings editusers editvalues
+               editversions editwhines editworkflow enter_bug page post_bug
+               process_bug query quips report reports request sanitycheck
+               search_plugin show_activity show_bug showdependencygraph
+               showdependencytree summarize_time userprefs votes);
+
+foreach my $page (@pages) {
+    $sel->open_ok("/$config->{bugzilla_installation}/${page}.cgi");
+    if ($page ne 'votes' || $config->{test_extensions}) {
+        $sel->title_is("Log in to Bugzilla");
+    }
+    else {
+        $sel->title_is("Extension Disabled");
+    }
+}
+
+# Those have parameters passed to the page, so we put them here separately.
+
+@pages = ("query.cgi?format=report-table", "query.cgi?format=report-graph",
+          "votes.cgi?action=show_user", "votes.cgi?action=show_bug");
+
+foreach my $page (@pages) {
+    $sel->open_ok("/$config->{bugzilla_installation}/$page");
+    if ($page !~ /^votes/ || $config->{test_extensions}) {
+        $sel->title_is("Log in to Bugzilla");
+    }
+    else {
+        $sel->title_is("Extension Disabled");
+    }
+}
+
+# These pages should still be accessible.
+
+@pages = ("config.cgi", "createaccount.cgi", "index.cgi", "relogin.cgi",
+          "token.cgi?a=reqpw&loginname=" . $config->{unprivileged_user_login});
+
+foreach my $page (@pages) {
+    $sel->open_ok("/$config->{bugzilla_installation}/$page");
+    $sel->title_isnt("Log in to Bugzilla");
+}
+
+# Turn off 'requirelogin'.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "User Authentication" => {"requirelogin-off" => undef} });
+logout($sel);
+
+# Make sure we can access random pages again.
+$sel->click_ok("link=Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_isnt("Log in to Bugzilla");
diff --git a/xt/selenium/sanity_check.t b/xt/selenium/sanity_check.t
new file mode 100644 (file)
index 0000000..93b039d
--- /dev/null
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Sanity Check", undef, "Go to Sanity Check (no parameter)");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Sanity Check", "Display sanitycheck.cgi");
+$sel->is_text_present_ok("Sanity check completed.", undef, "Page displayed correctly");
+
+my @args = qw(rebuildvotecache createmissinggroupcontrolmapentries repair_creation_date
+              repair_bugs_fulltext remove_invalid_bug_references repair_bugs_fulltext
+              remove_invalid_attach_references remove_old_whine_targets rescanallBugMail);
+
+foreach my $arg (@args) {
+    $sel->open_ok("/$config->{bugzilla_installation}/sanitycheck.cgi?$arg=1");
+    $sel->title_is("Suspicious Action", "Calling sanitycheck.cgi with no token triggers a confirmation page");
+    $sel->click_ok("confirm", "Confirm the action");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Sanity Check", "Calling sanitycheck.cgi with $arg=1");
+    if ($arg eq 'rescanallBugMail') {
+        # sanitycheck.cgi always stops after looking for unsent bugmail. So we cannot rely on
+        # "Sanity check completed." to determine if an error has been thrown or not.
+        $sel->is_text_present_ok("found with possibly unsent mail", undef, "Look for unsent bugmail");
+        ok(!$sel->is_text_present("Software error"), "No error thrown");
+    }
+    else {
+        $sel->is_text_present_ok("Sanity check completed.", undef, "Page displayed correctly");
+    }
+}
+
+logout($sel);
diff --git a/xt/selenium/saved_searches.t b/xt/selenium/saved_searches.t
new file mode 100644 (file)
index 0000000..a18b7fd
--- /dev/null
@@ -0,0 +1,117 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# If a saved search named 'SavedSearchTEST1' exists, remove it.
+
+log_in($sel, $config, 'QA_Selenium_TEST');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+
+if($sel->is_text_present("SavedSearchTEST1")) {
+    # There is no other way to identify this link (as they are all named "Forget").
+    $sel->click_ok('//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=SavedSearchTEST1")]');
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Search is gone");
+    $sel->is_text_present_ok("OK, the SavedSearchTEST1 search is gone.");
+}
+
+# Create a new saved search.
+
+open_advanced_search_page($sel);
+$sel->type_ok("short_desc", "test search");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->type_ok("save_newqueryname", "SavedSearchTEST1");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+my $text = trim($sel->get_text("message"));
+ok($text =~ /OK, you have a new search named SavedSearchTEST1./, "New search named SavedSearchTEST1 has been created");
+$sel->click_ok("link=SavedSearchTEST1");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: SavedSearchTEST1");
+
+# Remove the saved search from the page footer. It should no longer be displayed there.
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+
+$sel->is_text_present_ok("SavedSearchTEST1");
+$sel->uncheck_ok('//input[@type="checkbox" and @alt="SavedSearchTEST1"]');
+# $sel->value_is("//input[\@type='checkbox' and \@alt='SavedSearchTEST1']", "off");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+$text = trim($sel->get_text("message"));
+ok($text =~ /The changes to your saved searches have been saved./, "Saved searches changes have been saved");
+
+# Modify the saved search. Said otherwise, we should still be able to save
+# a new search with exactly the same name.
+
+open_advanced_search_page($sel);
+$sel->type_ok("short_desc", "bilboa");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+# As we said, this saved search should no longer be displayed in the page footer.
+ok(!$sel->is_text_present("SavedSearchTEST1"), "SavedSearchTEST1 is not present in the page footer");
+$sel->type_ok("save_newqueryname", "SavedSearchTEST1");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search updated");
+$text = trim($sel->get_text("message"));
+ok($text =~ /Your search named SavedSearchTEST1 has been updated./, "Saved searche SavedSearchTEST1 has been updated.");
+
+# Make sure our new criteria has been saved (let's edit the saved search).
+# As the saved search is no longer displayed in the footer, we have to go
+# to the "Preferences" page to edit it.
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+
+$sel->is_text_present_ok("SavedSearchTEST1");
+$sel->click_ok('//a[@href="buglist.cgi?cmdtype=dorem&remaction=run&namedcmd=SavedSearchTEST1"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: SavedSearchTEST1");
+$sel->click_ok("edit_search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search for bugs");
+$sel->value_is("short_desc", "bilboa");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->click_ok("forget_search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search is gone");
+$text = trim($sel->get_text("message"));
+ok($text =~ /OK, the SavedSearchTEST1 search is gone./, "The SavedSearchTEST1 search is gone.");
+logout($sel);
diff --git a/xt/selenium/search.t b/xt/selenium/search.t
new file mode 100644 (file)
index 0000000..c8fa9d7
--- /dev/null
@@ -0,0 +1,71 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use Test::More "no_plan";
+
+my ($sel, $config) = get_selenium();
+
+# TODO: This test really needs improvement. There is by far much more stuff
+# to test in this area.
+
+# First, a very trivial search, which returns no result.
+
+go_to_home($sel, $config);
+open_advanced_search_page($sel);
+$sel->type_ok("short_desc", "ois£jdfm#sd%fasd!fm", "Type a non-existent string in the bug summary field");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("Zarro Boogs found");
+
+# Display all available columns. Look for all bugs assigned to a user who doesn't exist.
+
+$sel->open_ok("/$config->{bugzilla_installation}/buglist.cgi?quicksearch=%40xx45ft&columnlist=all");
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("Zarro Boogs found");
+
+# Now some real tests.
+
+log_in($sel, $config, 'canconfirm');
+file_bug_in_product($sel, "TestProduct");
+my $bug_summary = "Update this summary with this bug ID";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "I'm supposed to appear in the coming buglist.");
+my $bug1_id = create_bug($sel, $bug_summary);
+$sel->click_ok("summary_edit_action");
+$bug_summary .= ": my ID is $bug1_id";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Updating bug summary....");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# Test pronoun substitution.
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections("bug_status");
+$sel->remove_all_selections("resolution");
+$sel->type_ok("short_desc", "my ID is $bug1_id");
+$sel->select_ok("f1", "label=Commenter");
+$sel->select_ok("o1", "label=is equal to");
+$sel->type_ok("v1", "%user%");
+$sel->click_ok("add_button");
+$sel->select_ok("f2", "label=Comment");
+$sel->select_ok("o2", "label=contains the string");
+$sel->type_ok("v2", "coming buglist");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("One bug found");
+$sel->is_text_present_ok("Update this summary with this bug ID: my ID is $bug1_id");
+logout($sel);
diff --git a/xt/selenium/security.t b/xt/selenium/security.t
new file mode 100644 (file)
index 0000000..b89ea11
--- /dev/null
@@ -0,0 +1,198 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium(CHROME_MODE);
+my $urlbase = $config->{bugzilla_installation};
+my $admin_user = $config->{admin_user_login};
+
+# Let's create a bug and attachment to play with.
+
+log_in($sel, $config, 'admin');
+file_bug_in_product($sel, "TestProduct");
+my $bug_summary = "Security checks";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "This bug will be used to test security fixes.");
+$sel->type_ok("data", $config->{attachment_file});
+$sel->type_ok("description", "simple patch, v1");
+$sel->click_ok("ispatch");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+
+#######################################################################
+# Security bug 38862.
+#######################################################################
+
+# No alternate host for attachments; cookies will be accessible.
+
+set_parameters($sel, { "Attachments" => {"allow_attachment_display-on" => undef,
+                                         "reset-attachment_base" => undef} });
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("link=Add an attachment");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Create New Attachment for Bug #$bug1_id");
+$sel->type_ok("attach_text", "<html>\n<head>\n<title>I want your cookies</title>\n<head>\n" .
+                             "<body>\n<script type='text/javascript'>document.write(document.cookie);</script>\n" .
+                             "</body>\n</html>", "Writing text into the attachment textarea");
+$sel->type_ok("description", "show my cookies");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "create"});
+my $alink = $sel->get_attribute('//a[@title="show my cookies"]@href');
+$alink =~ /id=(\d+)/;
+my $attach1_id = $1;
+$sel->click_ok("link=Attachment #$attach1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/Attachment $attach1_id Details for Bug $bug1_id/);
+$sel->click_ok("link=edit details");
+$sel->type_ok("contenttypeentry", "text/html");
+edit_bug($sel, $bug1_id, $bug_summary, {id => "update"});
+
+$sel->click_ok("link=show my cookies");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("I want your cookies");
+my @cookies = split(/[\s;]+/, $sel->get_body_text());
+my $nb_cookies = scalar @cookies;
+ok($nb_cookies, "Found $nb_cookies cookies:\n" . join("\n", @cookies));
+ok(!$sel->is_cookie_present("Bugzilla_login"), "Bugzilla_login not accessible");
+ok(!$sel->is_cookie_present("Bugzilla_logincookie"), "Bugzilla_logincookie not accessible");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+
+# Alternate host for attachments; no cookie should be accessible.
+
+set_parameters($sel, { "Attachments" => {"attachment_base" => {type  => "text",
+                                                               value => "http://127.0.0.1/$urlbase"}} });
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("link=show my cookies");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("I want your cookies");
+@cookies = split(/[\s;]+/, $sel->get_body_text());
+$nb_cookies = scalar @cookies;
+ok(!$nb_cookies, "No cookies found");
+ok(!$sel->is_cookie_present("Bugzilla_login"), "Bugzilla_login not accessible");
+ok(!$sel->is_cookie_present("Bugzilla_logincookie"), "Bugzilla_logincookie not accessible");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+
+set_parameters($sel, { "Attachments" => {"reset-attachment_base" => undef} });
+
+#######################################################################
+# Security bug 472362.
+#######################################################################
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+my $admin_cookie = $sel->get_value("token");
+logout($sel);
+
+log_in($sel, $config, 'editbugs');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+my $editbugs_cookie = $sel->get_value("token");
+
+# Using our own unused token is fine.
+
+$sel->open_ok("/$urlbase/userprefs.cgi?dosave=1&display_quips=off&token=$editbugs_cookie");
+$sel->title_is("General Preferences");
+$sel->is_text_present_ok("The changes to your general preferences have been saved");
+
+# Reusing a token must fail. They must all trigger the Suspicious Action warning.
+
+my @args = ("", "token=", "token=i123x", "token=$admin_cookie", "token=$editbugs_cookie");
+
+foreach my $arg (@args) {
+    $sel->open_ok("/$urlbase/userprefs.cgi?dosave=1&display_quips=off&$arg");
+    $sel->title_is("Suspicious Action");
+
+    if ($arg eq "token=$admin_cookie") {
+        $sel->is_text_present_ok("Generated by: admin <$admin_user>");
+        $sel->is_text_present_ok("This token has not been generated by you");
+    }
+    else {
+        $sel->is_text_present_ok("It looks like you didn't come from the right page");
+    }
+}
+logout($sel);
+
+#######################################################################
+# Security bug 529416.
+#######################################################################
+
+log_in($sel, $config, 'admin');
+file_bug_in_product($sel, "TestProduct");
+$sel->type_ok("alias", "secret_qa_bug_" . ($bug1_id + 1));
+my $bug_summary2 = "Private QA Bug";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "This private bug is used to test security fixes.");
+$sel->type_ok("dependson", $bug1_id);
+$sel->check_ok('//input[@name="groups" and @value="Master"]');
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+go_to_bug($sel, $bug1_id);
+$sel->is_text_present_ok("secret_qa_bug_$bug2_id");
+logout($sel);
+
+log_in($sel, $config, 'editbugs');
+go_to_bug($sel, $bug1_id);
+ok(!$sel->is_text_present("secret_qa_bug_$bug2_id"), "The alias 'secret_qa_bug_$bug2_id' is not visible for unauthorized users");
+$sel->is_text_present_ok($bug2_id);
+logout($sel);
+
+go_to_bug($sel, $bug1_id);
+ok(!$sel->is_text_present("secret_qa_bug_$bug2_id"), "The alias 'secret_qa_bug_$bug2_id' is not visible for logged out users");
+$sel->is_text_present_ok($bug2_id);
+
+#######################################################################
+# Security bug 472206.
+# Keep this test as the very last one as the File Saver will remain
+# open till the end of the script. Selenium is currently* unable
+# to interact with it and close it (* = 2.6.0).
+#######################################################################
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Attachments" => {"allow_attachment_display-off" => undef} });
+
+# Attachments are not viewable.
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("link=Details");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/Attachment \d+ Details for Bug $bug1_id/);
+$sel->is_text_present_ok("The attachment is not viewable in your browser due to security restrictions");
+$sel->click_ok("link=View");
+# Wait 1 second to give the browser a chance to display the attachment.
+# Do not use wait_for_page_to_load_ok() as the File Saver will never go away.
+sleep(1);
+ok(!$sel->is_text_present('@@'), "Patch not displayed");
+
+# Enable viewing attachments.
+
+set_parameters($sel, { "Attachments" => {"allow_attachment_display-on" => undef} });
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok('link=simple patch, v1');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("");
+$sel->is_text_present_ok('@@');
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/$bug1_id /);
+logout($sel);
diff --git a/xt/selenium/shared_searches.t b/xt/selenium/shared_searches.t
new file mode 100644 (file)
index 0000000..f9443fa
--- /dev/null
@@ -0,0 +1,199 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Set the querysharegroup param to be the canconfirm group.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Group Security" => {"querysharegroup" => {type => "select", value => "canconfirm"}} });
+
+# Create new saved search and call it 'Shared Selenium buglist'.
+
+$sel->type_ok("quicksearch_top", ":TestProduct Selenium");
+$sel->click_ok("find_top");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->type_ok("save_newqueryname", "Shared Selenium buglist");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+my $text = trim($sel->get_text("message"));
+ok($text =~ /OK, you have a new search named Shared Selenium buglist./, "New search named 'Shared Selenium buglist' has been created");
+
+# Retrieve the newly created saved search's internal ID and make sure it's displayed
+# in the footer by default.
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+my $ssname = $sel->get_attribute('//input[@type="checkbox" and @alt="Shared Selenium buglist"]@name');
+$ssname =~ /(?:link_in_footer_(\d+))/;
+my $saved_search1_id = $1;
+$sel->is_checked_ok("link_in_footer_$saved_search1_id");
+
+# As an admin, the "Add to footer" checkbox must be displayed, but unchecked by default.
+
+$sel->select_ok("share_$saved_search1_id", "label=canconfirm");
+ok(!$sel->is_checked("force_$saved_search1_id"), "Shared search not displayed in other users' footer by default");
+$sel->click_ok("force_$saved_search1_id");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+logout($sel);
+
+# Log in as the "canconfirm" user. The search shared by the admin must appear
+# in the footer.
+
+log_in($sel, $config, 'canconfirm');
+$sel->is_text_present_ok("Shared Selenium buglist");
+$sel->click_ok("link=Shared Selenium buglist");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: Shared Selenium buglist");
+# You cannot delete other users' saved searches.
+ok(!$sel->is_element_present("forget_search"), "'Forget...' button not available");
+
+# The name of the sharer must appear in the "Saved Searches" section.
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+$sel->is_text_present_ok($config->{admin_user_login});
+
+# Remove the shared search from your footer.
+
+$sel->is_checked_ok("link_in_footer_$saved_search1_id");
+$sel->click_ok("link_in_footer_$saved_search1_id");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+# Go to a page where the query name is unlikely to appear in the main page.
+$sel->click_ok("link=Permissions");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Permissions");
+ok(!$sel->is_text_present("Shared Selenium buglist"), "Shared query no longer displayed in the footer");
+
+# Create your own saved search, and share it with the canconfirm group.
+
+$sel->type_ok("quicksearch_top", ":TestProduct sw:helpwanted");
+$sel->click_ok("find_top");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->type_ok("save_newqueryname", "helpwanted");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+$text = trim($sel->get_text("message"));
+ok($text =~ /OK, you have a new search named helpwanted./, "New search named helpwanted has been created");
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+$ssname = $sel->get_attribute('//input[@type="checkbox" and @alt="helpwanted"]@name');
+$ssname =~ /(?:link_in_footer_(\d+))/;
+my $saved_search2_id = $1;
+# Our own saved searches are displayed in the footer by default.
+$sel->is_checked_ok("link_in_footer_$saved_search2_id");
+$sel->select_ok("share_$saved_search2_id", "label=canconfirm");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+logout($sel);
+
+# Log in as admin again. The other user is not a blesser for the 'canconfirm'
+# group, and so his shared search must not be displayed by default. But it
+# must still be available and can be added to the footer, if desired.
+
+log_in($sel, $config, 'admin');
+ok(!$sel->is_text_present("helpwanted"), "No 'helpwanted' shared search displayed");
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+$sel->is_text_present_ok("helpwanted");
+$sel->is_text_present_ok($config->{canconfirm_user_login});
+
+ok(!$sel->is_checked("link_in_footer_$saved_search2_id"), "Shared query available but not displayed");
+$sel->click_ok("link_in_footer_$saved_search2_id");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+# This query is now available from the footer.
+$sel->click_ok("link=helpwanted");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: helpwanted");
+
+# Remove the 'Shared Selenium buglist' query.
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+# There is no better way to identify the link
+$sel->click_ok('//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=Shared%20Selenium%20buglist")]',
+               undef, "Deleting the 'Shared Selenium buglist' search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search is gone");
+$text = trim($sel->get_text("message"));
+ok($text =~ /OK, the Shared Selenium buglist search is gone./, "The 'Shared Selenium buglist' search is gone");
+logout($sel);
+
+# Make sure that the 'helpwanted' query is not shared with the QA_Selenium_TEST
+# user as he doesn't belong to the 'canconfirm' group.
+
+log_in($sel, $config, 'QA_Selenium_TEST');
+ok(!$sel->is_text_present("helpwanted"), "The 'helpwanted' query is not displayed in the footer");
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+ok(!$sel->is_text_present("helpwanted"), "The 'helpwanted' query is not shared with this user");
+logout($sel);
+
+# Now remove the 'helpwanted' saved search.
+
+log_in($sel, $config, 'canconfirm');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Saved Searches");
+ok(!$sel->is_text_present("Shared Selenium buglist"), "The 'Shared Selenium buglist' is no longer available");
+$sel->click_ok('//a[contains(@href,"buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=helpwanted")]',
+               undef, "Deleting the 'helpwanted' search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search is gone");
+$text = trim($sel->get_text("message"));
+ok($text =~ /OK, the helpwanted search is gone./, "The 'helpwanted' search is gone");
+logout($sel);
diff --git a/xt/selenium/show_all_products.t b/xt/selenium/show_all_products.t
new file mode 100644 (file)
index 0000000..8945546
--- /dev/null
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} });
+
+# Do not use file_bug_in_product() because our goal here is not to file
+# a bug but to check what is present in the UI, and also to make sure
+# that we get exactly the right page with the right information.
+#
+# The admin is not a member of the "QA‑Selenium‑TEST" group, and so
+# cannot see the "QA‑Selenium‑TEST" product.
+
+$sel->click_ok("link=New");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Select Classification");
+my $full_text = trim($sel->get_body_text());
+ok($full_text =~ /All: Show all products/, "The 'All' link is displayed");
+$sel->click_ok("link=All");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Select Product");
+ok(!$sel->is_text_present("QA-Selenium-TEST"), "The QA-Selenium-TEST product is not displayed");
+logout($sel);
+
+# Same steps, but for a member of the "QA‑Selenium‑TEST" group.
+# The "QA‑Selenium‑TEST" product must be visible to him.
+
+log_in($sel, $config, 'QA_Selenium_TEST');
+$sel->click_ok("link=New");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Select Classification");
+$sel->click_ok("link=All");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Select Product");
+$sel->is_text_present_ok("QA-Selenium-TEST");
+$sel->click_ok('//a[contains(@href, "product=QA-Selenium-TEST")]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Enter Bug: QA-Selenium-TEST");
+logout($sel);
diff --git a/xt/selenium/shutdown.t b/xt/selenium/shutdown.t
new file mode 100644 (file)
index 0000000..8751cd6
--- /dev/null
@@ -0,0 +1,77 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "General" => {shutdownhtml => {type  => "text",
+                                                      value => "I'm down (set by test_shutdown.t)" }
+                                    } });
+
+# None of the following pages should be accessible when Bugzilla is down.
+
+my @pages = qw(admin attachment buglist chart colchange config createaccount
+               describecomponents describekeywords duplicates
+               editclassifications editcomponents editfields editflagtypes
+               editgroups editkeywords editmilestones editproducts editsettings
+               editusers editvalues editversions editwhines editworkflow
+               enter_bug index page post_bug process_bug query quips relogin
+               report reports request sanitycheck search_plugin show_activity
+               show_bug showdependencygraph showdependencytree summarize_time
+               token userprefs votes xmlrpc);
+
+foreach my $page (@pages) {
+    $sel->open_ok("/$config->{bugzilla_installation}/${page}.cgi");
+    $sel->title_is("Bugzilla is Down");
+}
+
+# Those have parameters passed to the page, so we put them here separately.
+
+@pages = ("query.cgi?format=report-table", "query.cgi?format=report-graph",
+          "votes.cgi?action=show_user", "votes.cgi?action=show_bug");
+
+foreach my $page (@pages) {
+    $sel->open_ok("/$config->{bugzilla_installation}/$page");
+    $sel->title_is("Bugzilla is Down");
+}
+
+# Clear 'shutdownhtml', to re-enable Bugzilla.
+# At this point, the admin has been logged out. We cannot use log_in(),
+# nor set_parameters(), due to shutdownhtml being active.
+
+$sel->open_ok("/$config->{bugzilla_installation}/editparams.cgi");
+$sel->title_is("Log in to Bugzilla");
+$sel->type_ok("Bugzilla_login", $config->{admin_user_login}, "Enter admin login name");
+$sel->type_ok("Bugzilla_password", $config->{admin_user_passwd}, "Enter admin password");
+$sel->click_ok("log_in");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Configuration: Required Settings");
+$sel->click_ok("link=General");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Configuration: General");
+$sel->type_ok("shutdownhtml", "");
+$sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Parameters Updated");
+
+# Accessing index.cgi should work again now.
+
+$sel->click_ok("link=Home");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugzilla Main Page");
+logout($sel);
diff --git a/xt/selenium/status_whiteboard.t b/xt/selenium/status_whiteboard.t
new file mode 100644 (file)
index 0000000..3ddda7b
--- /dev/null
@@ -0,0 +1,118 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, {'Bug Fields' => {'usestatuswhiteboard-on' => undef}});
+
+# Make sure the status whiteboard is displayed and add stuff to it.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "TestComponent");
+my $bug_summary = "white and black";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "This bug is to test the status whiteboard");
+my $bug1_id = create_bug($sel, $bug_summary);
+$sel->is_text_present_ok("Whiteboard:");
+$sel->type_ok("status_whiteboard", "[msg from test_status_whiteboard.t: x77v]");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "TestComponent");
+my $bug_summary2 = "WTC";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "bugzillation!");
+my $bug2_id = create_bug($sel, $bug_summary2);
+$sel->type_ok("status_whiteboard", "[msg from test_status_whiteboard.t: x77v]");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+# Now search these bugs above using data being in the status whiteboard,
+# and save the query.
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections_ok("product");
+$sel->remove_all_selections_ok("bug_status");
+$sel->type_ok("status_whiteboard", "x77v");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("2 bugs found");
+$sel->type_ok("save_newqueryname", "sw-x77v");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+my $text = trim($sel->get_text("message"));
+ok($text =~ /you have a new search named sw-x77v/, 'Saved search correctly saved');
+
+# Make sure the saved query works.
+
+$sel->click_ok("link=sw-x77v");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: sw-x77v");
+$sel->is_text_present_ok("2 bugs found");
+
+# The status whiteboard should no longer be displayed in both the query
+# and bug view pages (query.cgi and show_bug.cgi) when usestatuswhiteboard
+# is off.
+
+set_parameters($sel, {'Bug Fields' => {'usestatuswhiteboard-off' => undef}});
+$sel->click_ok("link=Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search for bugs");
+ok(!$sel->is_text_present("Whiteboard:"), "Whiteboard label no longer displayed in the search page");
+go_to_bug($sel, $bug1_id);
+ok(!$sel->is_text_present("Whiteboard:"), "Whiteboard label no longer displayed in the bug page");
+
+# Queries based on the status whiteboard should still work when
+# the parameter is off.
+
+$sel->click_ok("link=sw-x77v");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: sw-x77v");
+$sel->is_text_present_ok("2 bugs found");
+
+# Turn on usestatuswhiteboard again as some other scripts may expect the status
+# whiteboard to be available by default.
+
+set_parameters($sel, {'Bug Fields' => {'usestatuswhiteboard-on' => undef}});
+
+# Clear the status whiteboard and delete the saved search.
+
+$sel->click_ok("link=sw-x77v");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: sw-x77v");
+$sel->is_text_present_ok("2 bugs found");
+$sel->click_ok("mass_change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->click_ok("check_all");
+$sel->type_ok("status_whiteboard", "");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bugs processed");
+
+$sel->click_ok("link=sw-x77v");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: sw-x77v");
+$sel->is_text_present_ok("Zarro Boogs found");
+$sel->click_ok("forget_search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search is gone");
+$sel->is_text_present_ok("OK, the sw-x77v search is gone.");
+logout($sel);
diff --git a/xt/selenium/strict_isolation.t b/xt/selenium/strict_isolation.t
new file mode 100644 (file)
index 0000000..ecd72b2
--- /dev/null
@@ -0,0 +1,145 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+my $qa_user = $config->{QA_Selenium_TEST_user_login};
+my $no_privs_user = $config->{unprivileged_user_login};
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Group Security" => {"strict_isolation-on" => undef} });
+
+# Restrict the bug to the "Master" group, so that we can check that only
+# allowed people can be CC'ed to the bug.
+
+file_bug_in_product($sel, 'Another Product');
+$sel->select_ok("component", "label=c2");
+$sel->select_ok("version", "label=Another2");
+my $bug_summary = "Test isolation";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Unallowed users refused");
+my $master_gid = $sel->get_attribute('//input[@type="checkbox" and @name="groups" and @value="Master"]@id');
+$sel->check_ok($master_gid);
+$master_gid =~ s/group_//;
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# At that point, CANEDIT is off and so everybody can be CC'ed to the bug.
+
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", "$qa_user, $no_privs_user");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+$sel->click_ok("cc_edit_area_showhide");
+$sel->add_selection_ok("cc", "label=$no_privs_user");
+$sel->add_selection_ok("cc", "label=$qa_user");
+$sel->check_ok("removecc");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# Now enable CANEDIT for the "Master" group. This will enable strict isolation
+# for the product.
+
+edit_product($sel, "Another Product");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Group Controls for Another Product");
+$sel->check_ok("canedit_$master_gid");
+$sel->click_ok("submit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Update group access controls for Another Product");
+
+# Non-members can no longer be CC'ed to the bug.
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", $no_privs_user);
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Invalid User Group");
+$sel->is_text_present_ok("User '$no_privs_user' is not able to edit the 'Another Product' Product");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", $qa_user);
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Invalid User Group");
+$sel->is_text_present_ok("User '$qa_user' is not able to edit the 'Another Product' Product");
+
+# Now set QA_Selenium_TEST user as a member of the Master group.
+
+go_to_admin($sel);
+$sel->click_ok("link=Users");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search users");
+$sel->type_ok("matchstr", $qa_user);
+$sel->select_ok("matchtype", "label=exact (find this user)");
+$sel->click_ok("search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit user QA-Selenium-TEST <$qa_user>");
+$sel->check_ok("group_$master_gid");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User $qa_user updated");
+
+# The QA_Selenium_TEST user can now be CC'ed to the bug.
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", $qa_user);
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->click_ok("cc_edit_area_showhide");
+$sel->add_selection_ok("cc", "label=$qa_user");
+$sel->check_ok("removecc");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+# The powerless user still cannot be CC'ed.
+
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", "$qa_user, $no_privs_user");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Invalid User Group");
+$sel->is_text_present_ok("User '$no_privs_user' is not able to edit the 'Another Product' Product");
+
+# Reset parameters back to defaults.
+
+set_parameters($sel, { "Group Security" => {"strict_isolation-off" => undef} });
+
+go_to_admin($sel);
+$sel->click_ok("link=Users");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search users");
+$sel->type_ok("matchstr", $qa_user);
+$sel->select_ok("matchtype", "label=exact (find this user)");
+$sel->click_ok("search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit user QA-Selenium-TEST <$qa_user>");
+$sel->uncheck_ok("group_$master_gid");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User $qa_user updated");
+
+edit_product($sel, "Another Product");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Group Controls for Another Product");
+$sel->uncheck_ok("canedit_$master_gid");
+$sel->click_ok("submit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Update group access controls for Another Product");
+logout($sel);
diff --git a/xt/selenium/sudo_sessions.t b/xt/selenium/sudo_sessions.t
new file mode 100644 (file)
index 0000000..5a1b7c9
--- /dev/null
@@ -0,0 +1,158 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Turn on the usevisibilitygroups param so that some users are invisible.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Group Security" => {"usevisibilitygroups-on" => undef} });
+
+# You can see all users from editusers.cgi, but once you leave this page,
+# usual group visibility restrictions apply and the "powerless" user cannot
+# be sudo'ed as he is in no group.
+
+go_to_admin($sel);
+$sel->click_ok("link=Users");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search users");
+$sel->type_ok("matchstr", $config->{unprivileged_user_login});
+$sel->select_ok("matchtype", "label=exact (find this user)");
+$sel->click_ok("search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit user no-privs <$config->{unprivileged_user_login}>");
+$sel->value_is("login", $config->{unprivileged_user_login});
+$sel->click_ok("link=Impersonate this user");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Begin sudo session");
+$sel->value_is("target_login", $config->{unprivileged_user_login});
+$sel->type_ok("reason", "Selenium test about sudo sessions");
+$sel->type_ok("password", $config->{admin_user_passwd}, "Enter admin password");
+$sel->click_ok('//input[@value="Begin Session"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Match Failed");
+my $error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg eq "$config->{unprivileged_user_login} does not exist or you are not allowed to see that user.",
+   "Cannot impersonate users you cannot see");
+
+# Turn off the usevisibilitygroups param so that all users are visible again.
+
+set_parameters($sel, { "Group Security" => {"usevisibilitygroups-off" => undef} });
+
+# The "powerless" user can now be sudo'ed.
+
+go_to_admin($sel);
+$sel->click_ok("link=Users");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search users");
+$sel->type_ok("matchstr", $config->{unprivileged_user_login});
+$sel->select_ok("matchtype", "label=exact (find this user)");
+$sel->click_ok("search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit user no-privs <$config->{unprivileged_user_login}>");
+$sel->value_is("login", $config->{unprivileged_user_login});
+$sel->click_ok("link=Impersonate this user");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Begin sudo session");
+$sel->value_is("target_login", $config->{unprivileged_user_login});
+$sel->type_ok("password", $config->{admin_user_passwd}, "Enter admin password");
+$sel->click_ok('//input[@value="Begin Session"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Sudo session started");
+my $text = trim($sel->get_text("message"));
+ok($text =~ /The sudo session has been started/, "The sudo session has been started");
+
+# Make sure this user is not an admin and has no privs at all, and that
+# he cannot access editusers.cgi (despite the sudoer can).
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("General Preferences");
+$sel->click_ok("link=Permissions");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Permissions");
+$sel->is_text_present_ok("There are no permission bits set on your account");
+# We access the page directly as there is no link pointing to it.
+$sel->open_ok("/$config->{bugzilla_installation}/editusers.cgi");
+$sel->title_is("Authorization Required");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /^Sorry, you aren't a member of the 'editusers' group/, "Not a member of the editusers group");
+$sel->click_ok("link=end session");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Sudo session complete");
+$sel->is_text_present_ok("Your sudo session has ended");
+
+# Try to access the sudo page directly, with no credentials.
+
+$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=begin-sudo&target_login=$config->{admin_user_login}");
+$sel->title_is("Password Required");
+
+# Now try to start a sudo session directly, with all required credentials.
+
+$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=begin-sudo&password=$config->{admin_user_passwd}&target_login=$config->{unprivileged_user_login}", undef, "Impersonate a user directly by providing all required data");
+# A direct access to the page is supposed to have no Referer header set,
+# which would trigger the "Untrusted Authentication Request" error, but
+# due to the way Selenium works, the Referer header is set and the
+# "Preparation Required" error is thrown instead. In any case, one of
+# those two errors must be thrown.
+my $title = $sel->get_title();
+ok($title eq "Untrusted Authentication Request" || $title eq "Preparation Required", $title);
+
+# Now try to sudo an admin, which is not allowed.
+
+$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=prepare-sudo&target_login=$config->{admin_user_login}");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Begin sudo session");
+$sel->value_is("target_login", $config->{admin_user_login});
+$sel->type_ok("reason", "Selenium hack");
+$sel->type_ok("password", $config->{admin_user_passwd}, "Enter admin password");
+$sel->click_ok('//input[@value="Begin Session"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Protected");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /^The user $config->{admin_user_login} may not be impersonated by sudoers/, "Cannot impersonate administrators");
+
+# Now try to sudo a non-existing user account, with no password.
+
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Begin sudo session");
+# Starting with 5.0, the password field is a type=password and is marked
+# "required". This means that we need to remove the required attribute from
+# the input so that it can still be checked by the backend code.
+my $script = q{
+    document.getElementById('password').removeAttribute('required');
+};
+$sel->run_script($script);
+$sel->type_ok("target_login", 'foo@bar.com');
+$sel->click_ok('//input[@value="Begin Session"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Password Required");
+
+# Same as above, but with your password.
+
+$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=prepare-sudo&target_login=foo\@bar.com");
+$sel->title_is("Begin sudo session");
+$sel->value_is("target_login", 'foo@bar.com');
+$sel->type_ok("password", $config->{admin_user_passwd}, "Enter admin password");
+$sel->click_ok('//input[@value="Begin Session"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Match Failed");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg eq 'foo@bar.com does not exist or you are not allowed to see that user.', "Cannot impersonate non-existing accounts");
+logout($sel);
diff --git a/xt/selenium/target_milestones.t b/xt/selenium/target_milestones.t
new file mode 100644 (file)
index 0000000..6c5cf63
--- /dev/null
@@ -0,0 +1,111 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields" => {"usetargetmilestone-on" => undef} });
+
+# Create a new milestone to the 'TestProduct' product.
+
+edit_product($sel, "TestProduct");
+$sel->click_ok("link=Edit milestones:");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'");
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Add Milestone to Product 'TestProduct'");
+$sel->type_ok("milestone", "TM1");
+$sel->type_ok("sortkey", "10");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Milestone Created");
+
+# Edit the milestone of bugs.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "TestComponent");
+my $bug_summary = "stone and rock";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "This bug is to test milestones");
+my $bug1_id = create_bug($sel, $bug_summary);
+$sel->is_text_present_ok("Target Milestone:");
+$sel->select_ok("target_milestone", "label=TM1");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# Query for bugs with the TM1 milestone.
+
+open_advanced_search_page($sel);
+$sel->is_text_present_ok("Target Milestone:");
+$sel->remove_all_selections_ok("product");
+$sel->add_selection_ok("product", "label=TestProduct");
+$sel->add_selection_ok("target_milestone", "label=TM1");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("One bug found");
+$sel->type_ok("save_newqueryname", "selenium_m0");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+my $text = trim($sel->get_text("message"));
+ok($text =~ /OK, you have a new search named selenium_m0./, "New search named selenium_m0 has been created");
+
+# Turn off milestones and check that the milestone field no longer appears in bugs.
+
+set_parameters($sel, { "Bug Fields" => {"usetargetmilestone-off" => undef} });
+
+$sel->click_ok("link=Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search for bugs");
+ok(!$sel->is_text_present("Target:"), "The target milestone field is no longer displayed in the search page");
+
+go_to_bug($sel, $bug1_id);
+ok(!$sel->is_text_present("Target Milestone:"), "The milestone field is no longer displayed in the bug page");
+
+# The existing query must still work despite milestones are off now.
+
+$sel->click_ok("link=selenium_m0");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List: selenium_m0");
+$sel->is_text_present_ok("One bug found");
+$sel->click_ok("forget_search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search is gone");
+$text = trim($sel->get_text("message"));
+ok($text =~ /OK, the selenium_m0 search is gone./, "The selenium_m0 search is gone");
+
+# Re-enable the usetargetmilestone parameter and delete the created
+# milestone from the Testproduct product.
+
+set_parameters($sel, { "Bug Fields" => {"usetargetmilestone-on" => undef} });
+
+edit_product($sel, "TestProduct");
+$sel->click_ok("link=Edit milestones:");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'");
+$sel->click_ok('//a[@href="editmilestones.cgi?action=del&product=TestProduct&milestone=TM1"]',
+               undef, "Deleting the TM1 milestone");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Milestone of Product 'TestProduct'");
+$text = trim($sel->get_body_text());
+ok($text =~ /There is 1 bug entered for this milestone/, "Warning displayed about 1 bug targetted to TM1");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Deleted");
+logout($sel);
diff --git a/xt/selenium/time_summary.t b/xt/selenium/time_summary.t
new file mode 100644 (file)
index 0000000..f60c952
--- /dev/null
@@ -0,0 +1,101 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Set the timetracking group to "editbugs", which is the default value for this parameter.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Group Security" => {"timetrackinggroup" => {type => "select", value => "editbugs"}} });
+
+# Add some Hours Worked to a bug so that we are sure at least one bug
+# will be present in our buglist below.
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "TestComponent");
+my $bug_summary = "Rocket science";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Time flies");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+$sel->type_ok("work_time", 2.6);
+$sel->type_ok("comment", "I did some work");
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+$sel->is_text_present_ok("I did some work");
+$sel->is_text_present_ok("Additional hours worked: 2.6");
+
+# Let's call summarize_time.cgi directly, with no parameters.
+
+$sel->open_ok("/$config->{bugzilla_installation}/summarize_time.cgi");
+$sel->title_is("No Bugs Selected");
+my $error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /You apparently didn't choose any bugs for viewing/, "No data displayed");
+
+# Search for bugs which have some value in the Hours Worked field.
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections("bug_status");
+$sel->select_ok("f1", "label=Hours Worked");
+$sel->select_ok("o1", "label=is greater than");
+$sel->type_ok("v1", "0");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("found");
+
+# Test dates passed to summarize_time.cgi.
+
+$sel->click_ok("timesummary");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^Time Summary \(\d+ bugs selected\)/);
+$sel->check_ok("monthly");
+$sel->check_ok("detailed");
+$sel->type_ok("start_date", "2009-01-01");
+$sel->type_ok("end_date", "2009-04-30");
+$sel->click_ok("summarize");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^Time Summary \(\d+ bugs selected\)/);
+$sel->is_text_present_ok('regexp:Total of \d+\.\d+ hours worked');
+$sel->is_text_present_ok("2009-01-01 to 2009-01-31");
+$sel->is_text_present_ok("2009-02-01 to 2009-02-28");
+$sel->is_text_present_ok("2009-04-01 to 2009-04-30");
+
+$sel->type_ok("end_date", "2009-04-as");
+$sel->click_ok("summarize");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Illegal Date");
+$error_msg = trim($sel->get_text("error_msg"));
+ok($error_msg =~ /'2009-04-as' is not a legal date/, "Illegal end date");
+
+# Now display one bug only. We cannot do careful checks, because
+# the page sums up contributions made by the same user during the same
+# month, and so running this script several times per month would
+# break checks we may want to do (e.g. by making sure that the contribution
+# above has been taken into account). So we are just making sure that
+# the page is displayed and throws no error.
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("//a[contains(text(),'Summarize time')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Time Summary for Bug $bug1_id");
+$sel->check_ok("inactive");
+$sel->check_ok("owner");
+$sel->click_ok("summarize");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Time Summary for Bug $bug1_id");
+logout($sel);
diff --git a/xt/selenium/user_groups.t b/xt/selenium/user_groups.t
new file mode 100644 (file)
index 0000000..aa393d4
--- /dev/null
@@ -0,0 +1,249 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Administrative Policies" => {"allowuserdeletion-on" => undef} });
+
+# First delete test users, if not deleted correctly during a previous run.
+
+cleanup_users($sel);
+
+# The Master group inherits privs of the Slave group.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Master");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Group: Master");
+my $group_url = $sel->get_location();
+$group_url =~ /group=(\d+)$/;
+my $master_gid = $1;
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=Add Group");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add group");
+$sel->type_ok("name", "Slave");
+$sel->type_ok("desc", "Members of the Master group are also members of this group");
+$sel->uncheck_ok("isactive");
+ok(!$sel->is_checked("insertnew"), "Group not added to products by default");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("New Group Created");
+my $slave_gid = $sel->get_value("group_id");
+$sel->add_selection_ok("members_add", "label=Master");
+$sel->click_ok('//input[@value="Update Group"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Group: Slave");
+
+# Create users.
+
+go_to_admin($sel);
+$sel->click_ok("link=Users");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Search users');
+$sel->click_ok('link=add a new user');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Add user');
+$sel->type_ok('login', 'master@selenium.bugzilla.org');
+$sel->type_ok('name', 'master-user');
+$sel->type_ok('password', 'selenium', 'Enter password');
+$sel->type_ok('disabledtext', 'Not for common usage');
+$sel->click_ok('add');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Edit user master-user <master@selenium.bugzilla.org>');
+$sel->check_ok("//input[\@name='group_$master_gid']");
+$sel->click_ok('update');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('User master@selenium.bugzilla.org updated');
+$sel->is_text_present_ok('The account has been added to the Master group');
+
+$sel->click_ok("link=add a new user");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Add user');
+$sel->type_ok('login', 'slave@selenium.bugzilla.org');
+$sel->type_ok('name', 'slave-user');
+$sel->type_ok('password', 'selenium', 'Enter password');
+$sel->type_ok('disabledtext', 'Not for common usage');
+$sel->click_ok('add');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Edit user slave-user <slave@selenium.bugzilla.org>');
+$sel->check_ok("//input[\@name='group_$slave_gid']");
+$sel->click_ok('update');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('User slave@selenium.bugzilla.org updated');
+$sel->is_text_present_ok('The account has been added to the Slave group');
+
+$sel->click_ok("link=add a new user");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Add user');
+$sel->type_ok('login', 'reg@selenium.bugzilla.org');
+$sel->type_ok('name', 'reg-user');
+$sel->type_ok('password', 'selenium', 'Enter password');
+$sel->type_ok('disabledtext', 'Not for common usage');
+$sel->click_ok('add');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Edit user reg-user <reg@selenium.bugzilla.org>');
+
+# Disabled accounts are not listed by default.
+
+$sel->click_ok('link=find other users');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Search users');
+$sel->select_ok('is_enabled', 'label=Enabled');
+$sel->click_ok('search');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+ok(!$sel->is_text_present('master@selenium.bugzilla.org'), 'Inactive user account master-user not listed by default');
+ok(!$sel->is_text_present('slave@selenium.bugzilla.org'), 'Inactive user account slave-user not listed by default');
+ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'Inactive user account reg-user not displayed by default');
+
+# Now make sure group inheritance works correctly.
+
+$sel->click_ok('link=find other users');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Search users');
+$sel->check_ok('grouprestrict');
+$sel->select_ok('groupid', 'label=Master');
+$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('is_enabled', 'label=All');
+$sel->click_ok('search');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Master group');
+ok(!$sel->is_text_present('slave@selenium.bugzilla.org'), 'slave-user not in Master group');
+ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'reg-user not in Master group');
+
+$sel->click_ok('link=find other users');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Search users');
+$sel->check_ok('grouprestrict');
+$sel->select_ok('groupid', 'label=Slave');
+$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('is_enabled', 'label=All');
+$sel->click_ok('search');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Slave group');
+$sel->is_text_present_ok('slave@selenium.bugzilla.org', 'slave-user in Slave group');
+ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'reg-user not in Slave group');
+
+# Add a regular expression to the Slave group.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok('link=Slave');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Change Group: Slave');
+$sel->type_ok('regexp', '^reg\@.*$');
+$sel->click_ok('//input[@value="Update Group"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Group: Slave");
+
+# Test group inheritance again.
+
+go_to_admin($sel);
+$sel->click_ok("link=Users");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Search users');
+$sel->check_ok('grouprestrict');
+$sel->select_ok('groupid', 'label=Master');
+$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('is_enabled', 'label=All');
+$sel->click_ok('search');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Master group');
+ok(!$sel->is_text_present('slave@selenium.bugzilla.org'), 'slave-user not in Master group');
+ok(!$sel->is_text_present('reg@selenium.bugzilla.org'), 'reg-user not in Master group');
+
+$sel->click_ok('link=find other users');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is('Search users');
+$sel->check_ok('grouprestrict');
+$sel->select_ok('groupid', 'label=Slave');
+$sel->select_ok('matchtype', 'value=substr');
+$sel->select_ok('is_enabled', 'label=All');
+$sel->click_ok('search');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('master@selenium.bugzilla.org', 'master-user in Slave group');
+$sel->is_text_present_ok('slave@selenium.bugzilla.org', 'slave-user in Slave group');
+$sel->is_text_present_ok('reg@selenium.bugzilla.org', 'reg-user in Slave group');
+
+# Remove created users and groups.
+
+cleanup_users($sel);
+cleanup_groups($sel, $slave_gid);
+logout($sel);
+
+sub cleanup_users {
+    my $sel = shift;
+
+    go_to_admin($sel);
+    $sel->click_ok("link=Users");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Search users");
+    $sel->type_ok('matchstr', '(master|slave|reg)@selenium.bugzilla.org');
+    $sel->select_ok('matchtype', 'value=regexp');
+    $sel->select_ok('is_enabled', 'label=All');
+    $sel->click_ok("search");
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Select user");
+
+    foreach my $user ('master', 'slave', 'reg') {
+        my $login = $user . '@selenium.bugzilla.org';
+        next unless $sel->is_text_present($login);
+
+        $sel->click_ok("link=$login");
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_is("Edit user ${user}-user <$login>");
+        $sel->click_ok("delete");
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_is("Confirm deletion of user $login");
+        ok(!$sel->is_text_present('You cannot delete this user account'), 'The user can be safely deleted');
+        $sel->click_ok("delete");
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_is("User $login deleted");
+        $sel->click_ok('link=show the user list again');
+        $sel->wait_for_page_to_load_ok(WAIT_TIME);
+        $sel->title_is('Select user');
+    }
+}
+
+sub cleanup_groups {
+    my ($sel, $slave_gid) = @_;
+
+    go_to_admin($sel);
+    $sel->click_ok("link=Groups");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Edit Groups");
+    $sel->click_ok("//a[\@href='editgroups.cgi?action=del&group=$slave_gid']");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Delete group 'Slave'");
+    $sel->is_text_present_ok("Do you really want to delete this group?");
+    ok(!$sel->is_element_present("removeusers"), 'No direct members in this group');
+    $sel->click_ok("delete");
+    $sel->wait_for_page_to_load(WAIT_TIME);
+    $sel->title_is("Group Deleted");
+    $sel->is_text_present_ok("The group Slave has been deleted.");
+}
diff --git a/xt/selenium/user_matching.t b/xt/selenium/user_matching.t
new file mode 100644 (file)
index 0000000..90c2dc6
--- /dev/null
@@ -0,0 +1,188 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'tweakparams');
+set_parameters($sel, { "User Matching"  => {"usemenuforusers-off" => undef,
+                                            "maxusermatches"      => {type => 'text', value => '0'},
+                                            "confirmuniqueusermatch-on" => undef},
+                       "Group Security" => {"usevisibilitygroups-off" => undef}
+                     });
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "TestComponent");
+my $bug_summary = "Today is Tuesday";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Poker Face");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# We enter an incomplete email address. process_bug.cgi must ask
+# for confirmation as confirmuniqueusermatch is turned on.
+
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", $config->{unprivileged_user_login_truncated});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Confirm Match");
+$sel->is_text_present_ok("$config->{unprivileged_user_login_truncated} matched");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id/);
+$sel->click_ok("cc_edit_area_showhide");
+
+# We now enter a complete and valid email address, so it must be accepted.
+# confirmuniqueusermatch = 1 must not trigger the confirmation page as we
+# type the complete email address.
+
+$sel->type_ok("newcc", $config->{unprivileged_user_login});
+edit_bug_and_return($sel, $bug1_id, $bug_summary);
+
+# Now test wildcards ("*"). Due to confirmuniqueusermatch being turned on,
+# a confirmation page must be displayed.
+
+$sel->click_ok("cc_edit_area_showhide");
+$sel->type_ok("newcc", "$config->{unprivileged_user_login_truncated}*");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Confirm Match");
+$sel->is_text_present_ok("<$config->{unprivileged_user_login}>");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id/);
+$sel->click_ok("cc_edit_area_showhide");
+
+# This will return more than one account.
+
+$sel->type_ok("newcc", "*$config->{common_email}");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Confirm Match");
+$sel->is_text_present_ok("*$config->{common_email} matched:");
+
+# Now restrict 'maxusermatches'.
+
+set_parameters($sel, { "User Matching" => {"maxusermatches" => {type => 'text', value => '1'}} });
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("cc_edit_area_showhide");
+
+# Several user accounts match this partial email address. Due to
+# maxusermatches = 1, no email address is suggested.
+
+$sel->type_ok("newcc", "*$config->{common_email}");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Match Failed");
+$sel->is_text_present_ok("matches multiple users");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id/);
+$sel->click_ok("cc_edit_area_showhide");
+
+# We now type a complete and valid email address, so no confirmation
+# page should be displayed.
+
+$sel->type_ok("newcc", $config->{unprivileged_user_login});
+edit_bug($sel, $bug1_id, $bug_summary);
+
+# Now turn on group visibility. It involves important security checks.
+
+set_parameters($sel, { "User Matching"  => {"maxusermatches" => {type => 'text', value => '2'}},
+                       "Group Security" => {"usevisibilitygroups-on" => undef}
+                     });
+
+# By default, groups are not visible to themselves, so we have to enable this.
+# The tweakparams user has not enough privs to do it himself.
+
+logout($sel);
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->click_ok("link=tweakparams");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Change Group: tweakparams");
+
+my @groups = $sel->get_select_options("visible_from_add");
+if (grep {$_ eq 'tweakparams'} @groups) {
+    $sel->add_selection_ok("visible_from_add", "label=tweakparams");
+    $sel->click_ok('//input[@value="Update Group"]');
+    $sel->wait_for_page_to_load_ok(WAIT_TIME);
+    $sel->title_is("Change Group: tweakparams");
+}
+logout($sel);
+log_in($sel, $config, 'tweakparams');
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("cc_edit_area_showhide");
+
+# We are not in the same groups as the unprivileged user, so we cannot see him.
+
+$sel->type_ok("newcc", $config->{unprivileged_user_login_truncated});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Match Failed");
+$sel->is_text_present_ok("$config->{unprivileged_user_login_truncated} did not match anything");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id/);
+$sel->click_ok("cc_edit_area_showhide");
+
+# This will return too many users (there are at least always three:
+# you, the admin and the permanent user (who has admin privs too)).
+
+$sel->type_ok("newcc", $config->{common_email});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Confirm Match");
+$sel->is_text_present_ok("$config->{common_email} matched more than the maximum of 2 users");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id/);
+$sel->click_ok("cc_edit_area_showhide");
+
+# We can always see ourselves.
+
+$sel->type_ok("newcc", $config->{tweakparams_user_login_truncated});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Confirm Match");
+$sel->is_text_present_ok("<$config->{tweakparams_user_login}>");
+
+# Now test user menus. It must NOT display users we are not allowed to see.
+
+set_parameters($sel, { "User Matching" => {"usemenuforusers-on" => undef} });
+
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("cc_edit_area_showhide");
+my @cc = $sel->get_select_options("newcc");
+ok(!grep($_ =~ /$config->{unprivileged_user_login}/, @cc), "$config->{unprivileged_user_login} is not visible");
+ok(!grep($_ =~ /$config->{canconfirm_user_login}/, @cc), "$config->{canconfirm_user_login} is not visible");
+ok(grep($_ =~ /$config->{admin_user_login}/, @cc), "$config->{admin_user_login} is visible");
+ok(grep($_ =~ /$config->{tweakparams_user_login}/, @cc), "$config->{tweakparams_user_login} is visible");
+
+# Reset paramters.
+
+set_parameters($sel, { "User Matching"  => {"usemenuforusers-off" => undef,
+                                            "maxusermatches"      => {type => 'text', value => '0'},
+                                            "confirmuniqueusermatch-off" => undef},
+                       "Group Security" => {"usevisibilitygroups-off" => undef}
+                     });
+logout($sel);
diff --git a/xt/selenium/user_preferences.t b/xt/selenium/user_preferences.t
new file mode 100644 (file)
index 0000000..0d7d87a
--- /dev/null
@@ -0,0 +1,225 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Update default user preferences.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Default Preferences");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+$sel->uncheck_ok("skin-enabled");
+$sel->value_is("skin-enabled", "off");
+$sel->check_ok("state_addselfcc-enabled");
+$sel->select_ok("state_addselfcc", "label=Never");
+$sel->check_ok("post_bug_submit_action-enabled");
+$sel->select_ok("post_bug_submit_action", "label=Show the updated bug");
+$sel->uncheck_ok("zoom_textareas-enabled");
+$sel->select_ok("zoom_textareas", "label=Off");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+
+# Update own user preferences. Some of them are not editable.
+
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("General Preferences");
+ok(!$sel->is_editable("skin"), "The 'skin' user preference is not editable");
+$sel->select_ok("state_addselfcc", "label=Site Default (Never)");
+$sel->select_ok("post_bug_submit_action", "label=Site Default (Show the updated bug)");
+ok(!$sel->is_editable("zoom_textareas"), "The 'zoom_textareas' user preference is not editable");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("General Preferences");
+
+# File a bug in the 'TestProduct' product. The form fields must follow user prefs.
+
+file_bug_in_product($sel, 'TestProduct');
+$sel->value_is("cc", "");
+my $bug_summary = "First bug created";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "I'm not in the CC list.");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+$sel->value_is("addselfcc", "off");
+$sel->type_ok("tag", "sel-tmp");
+$sel->select_ok("bug_status", "label=IN_PROGRESS");
+edit_bug($sel, $bug1_id, $bug_summary);
+$sel->click_ok("summary_edit_action");
+$sel->value_is("short_desc", $bug_summary);
+$sel->value_is("addselfcc", "off");
+
+# Create a saved search for the 'sel-tmp' tag.
+
+$sel->type_ok("quicksearch_top", "tag:sel-tmp");
+$sel->click_ok("find_top");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->type_ok("save_newqueryname", "sel-tmp");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search created");
+$sel->is_text_present_ok("OK, you have a new search named sel-tmp");
+
+# Leave this page to avoid clicking on the wrong 'sel-tmp' link.
+go_to_home($sel, $config);
+$sel->click_ok("link=sel-tmp");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: sel-tmp");
+$sel->is_text_present_ok("One bug found");
+
+# File another bug in the 'TestProduct' product.
+
+file_bug_in_product($sel, 'TestProduct');
+$sel->value_is("cc", "");
+my $bug_summary2 = "My second bug";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "Still not in the CC list");
+my $bug2_id = create_bug($sel, $bug_summary2);
+$sel->value_is("addselfcc", "off");
+$sel->type_ok("tag", "sel-tmp");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+$sel->click_ok("link=sel-tmp");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: sel-tmp");
+$sel->is_text_present_ok("2 bugs found");
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->type_ok("comment", "The next bug I should see is this one.");
+edit_bug($sel, $bug1_id, $bug_summary);
+$sel->click_ok("summary_edit_action");
+$sel->value_is("short_desc", "First bug created");
+$sel->is_text_present_ok("The next bug I should see is this one.");
+
+# Remove the saved search. The tag itself still exists.
+
+$sel->click_ok("link=sel-tmp");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: sel-tmp");
+$sel->click_ok("forget_search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Search is gone");
+$sel->is_text_present_ok("OK, the sel-tmp search is gone");
+
+# Remove the tag from bugs.
+
+$sel->type_ok("quicksearch_top", "tag:sel-tmp");
+$sel->click_ok("find_top");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("2 bugs found");
+# We cannot remove tags from several bugs at once (bug 791584).
+go_to_bug($sel, $bug1_id);
+$sel->type_ok("tag", "");
+edit_bug($sel, $bug1_id, $bug_summary);
+
+go_to_bug($sel, $bug2_id);
+$sel->type_ok("tag", "");
+edit_bug($sel, $bug2_id, $bug_summary2);
+
+$sel->type_ok("quicksearch_top", "tag:sel-tmp");
+$sel->click_ok("find_top");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("Zarro Boogs found");
+logout($sel);
+
+# Edit own user preferences, now as an unprivileged user.
+
+log_in($sel, $config, 'unprivileged');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("General Preferences");
+ok(!$sel->is_editable("skin"), "The 'skin' user preference is not editable");
+$sel->select_ok("state_addselfcc", "label=Always");
+$sel->select_ok("post_bug_submit_action", "label=Show next bug in my list");
+ok(!$sel->is_editable("zoom_textareas"), "The 'zoom_textareas' user preference is not editable");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("General Preferences");
+
+# Create a new search named 'my_list'.
+
+open_advanced_search_page($sel);
+$sel->remove_all_selections_ok("product");
+$sel->add_selection_ok("product", "TestProduct");
+$sel->remove_all_selections_ok("bug_status");
+$sel->select_ok("bug_id_type", "label=only included in");
+$sel->type_ok("bug_id", "$bug1_id , $bug2_id");
+$sel->select_ok("order", "label=Bug Number");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("2 bugs found");
+$sel->type_ok("save_newqueryname", "my_list");
+$sel->click_ok("remember");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Search created");
+$sel->is_text_present_ok("OK, you have a new search named my_list");
+
+# Editing bugs should follow user preferences.
+
+$sel->click_ok("link=my_list");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: my_list");
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->value_is("addselfcc", "on");
+$sel->type_ok("comment", "I should be CC'ed and then I should see the next bug.");
+edit_bug($sel, $bug2_id, $bug_summary2);
+$sel->is_text_present_ok("The next bug in your list is bug $bug2_id");
+ok(!$sel->is_text_present("I should see the next bug"), "The updated bug is no longer displayed");
+# The user has no privs, so the short_desc field is not present.
+$sel->is_text_present("short_desc", "My second bug");
+$sel->value_is("addselfcc", "on");
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->is_text_present("1 user including you");
+
+# Delete the saved search and log out.
+
+$sel->click_ok("link=my_list");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List: my_list");
+$sel->click_ok("forget_search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Search is gone");
+$sel->is_text_present_ok("OK, the my_list search is gone");
+logout($sel);
+
+# Restore default user preferences.
+
+log_in($sel, $config, 'admin');
+go_to_admin($sel);
+$sel->click_ok("link=Default Preferences");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+$sel->check_ok("skin-enabled");
+$sel->uncheck_ok("post_bug_submit_action-enabled");
+$sel->select_ok("post_bug_submit_action", "label=Do Nothing");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Default Preferences");
+logout($sel);
diff --git a/xt/selenium/user_privs.t b/xt/selenium/user_privs.t
new file mode 100644 (file)
index 0000000..f487928
--- /dev/null
@@ -0,0 +1,60 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Create a new bug. As the reporter, some forms are editable to you.
+# But as you don't have editbugs privs, you cannot edit everything.
+
+log_in($sel, $config, 'unprivileged');
+file_bug_in_product($sel, 'TestProduct');
+ok(!$sel->is_editable("assigned_to"), "The assignee field is not editable");
+my $bug_summary = "Greetings from a powerless user";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "File a bug with an empty CC list");
+my $bug1_id = create_bug($sel, $bug_summary);
+logout($sel);
+
+# Some checks while being logged out.
+
+go_to_bug($sel, $bug1_id);
+ok(!$sel->is_element_present("commit"), "Button 'Commit' not available");
+my $text = trim($sel->get_text("//fieldset"));
+ok($text =~ /You need to log in before you can comment on or make changes to this bug./,
+   "Addl. comment box not displayed");
+
+# Don't call log_in() here. We explicitly want to use the "log in" link
+# in the addl. comment box.
+
+$sel->click_ok("link=log in");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Log in to Bugzilla");
+$sel->is_text_present_ok("Bugzilla needs a legitimate login and password to continue");
+$sel->type_ok("Bugzilla_login", $config->{unprivileged_user_login}, "Enter login name");
+$sel->type_ok("Bugzilla_password", $config->{unprivileged_user_passwd}, "Enter password");
+$sel->click_ok("log_in", undef, "Submit credentials");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id/, "Display bug $bug1_id");
+
+# Neither the (edit) link nor the hidden form must exist, at all.
+# But the 'Commit' button does exist.
+
+ok(!$sel->is_element_present("bz_assignee_edit_action"), "No (edit) link displayed for the assignee");
+ok(!$sel->is_element_present("assigned_to"), "No hidden assignee field available");
+$sel->is_element_present_ok("commit");
+logout($sel);
diff --git a/xt/selenium/votes.t b/xt/selenium/votes.t
new file mode 100644 (file)
index 0000000..e5a7c85
--- /dev/null
@@ -0,0 +1,233 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+unless ($config->{test_extensions}) {
+    ok(1, "this installation doesn't test extensions. Skipping test_votes.t completely.");
+    exit;
+}
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields"              => {"useclassification-off" => undef},
+                       "Administrative Policies" => {"allowbugdeletion-on"   => undef}
+                     });
+
+# Create a new product, so that we can safely play with vote settings.
+
+add_product($sel);
+$sel->type_ok("product", "Eureka");
+$sel->type_ok("description", "A great new product");
+$sel->type_ok("votesperuser", 10);
+$sel->type_ok("maxvotesperbug", 5);
+$sel->type_ok("votestoconfirm", 3);
+$sel->type_ok("component", "Pegasus");
+$sel->type_ok("comp_desc", "A constellation in the north hemisphere.");
+$sel->type_ok("initialowner", $config->{permanent_user}, "Setting the default owner");
+$sel->click_ok('add-product');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Created");
+
+# Create a new bug with the CONFIRMED status.
+
+file_bug_in_product($sel, 'Eureka');
+# CONFIRMED must be the default bug status for users with editbugs privs.
+$sel->selected_label_is("bug_status", "CONFIRMED");
+my $bug_summary = "Aries";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "1st constellation");
+my $bug1_id = create_bug($sel, $bug_summary);
+
+# Now vote for this bug.
+
+$sel->click_ok("link=vote");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Votes");
+# No comment :-/
+my $full_text = trim($sel->get_body_text());
+# OK, this is not the most robust regexp, but that's better than nothing.
+ok($full_text =~ /only 5 votes allowed per bug in this product/,
+   "Notice about the number of votes allowed per bug displayed");
+$sel->type_ok("bug_$bug1_id", 4);
+$sel->click_ok("change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Votes");
+$full_text = trim($sel->get_body_text());
+# OK, we may get a false positive if another product has the exact same numbers,
+# but I have no better idea to check this information.
+ok($full_text =~ /4 votes used out of 10 allowed/, "Display the number of votes used");
+
+# File a new bug, now as UNCONFIRMED. We will confirm it by popular votes.
+
+file_bug_in_product($sel, 'Eureka');
+$sel->select_ok("bug_status", "UNCONFIRMED");
+my $bug_summary2 = "Taurus";
+$sel->type_ok("short_desc", $bug_summary2);
+$sel->type_ok("comment", "2nd constellation");
+my $bug2_id = create_bug($sel, $bug_summary2);
+
+# Put enough votes on this bug to confirm it by popular votes.
+
+$sel->click_ok("link=vote");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Votes");
+$sel->type_ok("bug_$bug2_id", 5);
+$sel->click_ok("change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Votes");
+$sel->is_text_present_ok("Bug $bug2_id confirmed by number of votes");
+
+# File a third bug, again as UNCONFIRMED. We will confirm it
+# by decreasing the number required to confirm bugs by popular votes.
+
+file_bug_in_product($sel, 'Eureka');
+$sel->select_ok("bug_status", "UNCONFIRMED");
+my $bug_summary3 = "Gemini";
+$sel->type_ok("short_desc", $bug_summary3);
+$sel->type_ok("comment", "3rd constellation");
+my $bug3_id = create_bug($sel, $bug_summary3);
+
+# Vote for this bug, but remain below the threshold required
+# to confirm the bug by popular votes.
+# We also change votes set on other bugs for testing purposes.
+
+$sel->click_ok("link=vote");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Votes");
+$sel->type_ok("bug_$bug1_id", 2);
+$sel->type_ok("bug_$bug3_id", 2);
+$sel->click_ok("change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Votes");
+# Illegal change: max is 5 votes per bug!
+$sel->type_ok("bug_$bug2_id", 15);
+$sel->click_ok("change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Illegal Vote");
+my $text = trim($sel->get_text("error_msg"));
+ok($text =~ /You may only use at most 5 votes for a single bug in the Eureka product, but you are trying to use 15/,
+   "Too many votes per bug");
+
+# FIXME - We cannot use go_back_ok() here, because Firefox complains about
+# POST data not being stored in its cache. As a workaround, we go to
+# the bug we just visited and click the 'vote' link again.
+
+go_to_bug($sel, $bug3_id);
+$sel->click_ok("link=vote");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Votes");
+
+# Illegal change: max is 10 votes for this product!
+$sel->type_ok("bug_$bug2_id", 5);
+$sel->type_ok("bug_$bug1_id", 5);
+$sel->click_ok("change");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Illegal Vote");
+$text = trim($sel->get_text("error_msg"));
+ok($text =~ /You tried to use 12 votes in the Eureka product, which exceeds the maximum of 10 votes for this product/,
+   "Too many votes for this product");
+
+# Decrease the confirmation threshold so that $bug3 becomes confirmed.
+
+edit_product($sel, 'Eureka');
+$sel->type_ok("votestoconfirm", 2);
+$sel->click_ok("update-product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Updating Product 'Eureka'");
+$full_text = trim($sel->get_body_text());
+ok($full_text =~ /Updated number of votes needed to confirm a bug from 3 to 2/,
+   "Confirming the new number of votes to confirm");
+$sel->is_text_present_ok("Bug $bug3_id confirmed by number of votes");
+
+# Decrease the number of votes per bug so that $bug2 is updated.
+
+$sel->click_ok("link='Eureka'");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'Eureka'");
+$sel->type_ok("maxvotesperbug", 4);
+$sel->click_ok("update-product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Updating Product 'Eureka'");
+$full_text = trim($sel->get_body_text());
+ok($full_text =~ /Updated maximum votes per bug from 5 to 4/, "Confirming the new number of votes per bug");
+$sel->is_text_present_ok("removed votes for bug $bug2_id from " . $config->{admin_user_login}, undef,
+                         "Removed votes from the admin");
+
+# Go check that $bug2 has been correctly updated.
+
+$sel->click_ok("link=$bug2_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/$bug2_id /);
+$text = trim($sel->get_text("votes_container"));
+ok($text =~ /4 votes/, "4 votes remaining");
+
+# Decrease the number per user. Bugs should keep at least one vote,
+# i.e. not all votes are removed (which was the old behavior).
+
+edit_product($sel, "Eureka");
+$sel->type_ok("votesperuser", 5);
+$sel->click_ok("update-product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Updating Product 'Eureka'");
+$full_text = trim($sel->get_body_text());
+ok($full_text =~ /Updated votes per user from 10 to 5/, "Confirming the new number of votes per user");
+$sel->is_text_present_ok("removed votes for bug");
+
+# Go check that $bug3 has been correctly updated.
+
+$sel->click_ok("link=$bug3_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/$bug3_id /);
+$text = trim($sel->get_text("votes_container"));
+ok($text =~ /2 votes/, "2 votes remaining");
+
+# Now disable UNCONFIRMED.
+
+edit_product($sel, "Eureka");
+$sel->click_ok("allows_unconfirmed");
+$sel->click_ok("update-product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Updating Product 'Eureka'");
+$full_text = trim($sel->get_body_text());
+ok($full_text =~ /The product no longer allows the UNCONFIRMED status/, "Disable UNCONFIRMED");
+
+# File a new bug. UNCONFIRMED must not be listed as a valid bug status.
+
+file_bug_in_product($sel, "Eureka");
+ok(!scalar(grep {$_ eq "UNCONFIRMED"} $sel->get_select_options("bug_status")), "UNCONFIRMED not listed");
+my $bug_summary4 = "Cancer";
+$sel->type_ok("short_desc", $bug_summary4);
+$sel->type_ok("comment", "4th constellation");
+my $bug4_id = create_bug($sel, $bug_summary4);
+
+# Now delete the 'Eureka' product.
+
+go_to_admin($sel);
+$sel->click_ok("link=Products");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select product");
+$sel->click_ok('//a[@href="editproducts.cgi?action=del&product=Eureka"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Product 'Eureka'");
+$full_text = trim($sel->get_body_text());
+ok($full_text =~ /There are 4 bugs entered for this product/, "Display warning about existing bugs");
+ok($full_text =~ /Pegasus: A constellation in the north hemisphere/, "Display product description");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Deleted");
+logout($sel);
diff --git a/xt/webservice/bug_add_attachment.t b/xt/webservice/bug_add_attachment.t
new file mode 100644 (file)
index 0000000..f08e42c
--- /dev/null
@@ -0,0 +1,231 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use MIME::Base64 qw(encode_base64 decode_base64);
+use Test::More tests => 187;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ALIAS => random_string(20);
+use constant PRIVS_USER => 'QA_Selenium_TEST';
+
+sub attach {
+    my ($id, $override) = @_;
+    my %fields = (
+        ids  => [$id],
+        data => 'data-' . random_string(100),
+        file_name => 'file_name-' . random_string(60),
+        summary => 'summary-' . random_string(100),
+        content_type => 'text/plain',
+        comment => 'comment-' . random_string(100),
+    );
+
+    foreach my $key (keys %{ $override || {} }) {
+        my $value = $override->{$key};
+        if (defined $value) {
+            $fields{$key} = $value;
+        }
+        else {
+            delete $fields{$key};
+        }
+    }
+    return \%fields;
+}
+
+my ($public_bug, $private_bug) =
+    $xmlrpc->bz_create_test_bugs('private');
+my $public_id = $public_bug->{id};
+my $private_id = $private_bug->{id};
+
+my @tests = (
+    # Permissions
+    { args  => attach($public_id),
+      error => 'You must log in',
+      test  => 'Logged-out user cannot add an attachment to a public bug',
+    },
+    { args  => attach($private_id),
+      error => "You must log in",
+      test  => 'Logged-out user cannot add an attachment to a private bug',
+    },
+    { user  => 'editbugs',
+      args  => attach($private_id),
+      error => "not authorized to access",
+      test  => "Editbugs user can't add an attachment to a private bug",
+    },
+
+    # Test ID parameter
+    { user  => 'unprivileged',
+      args  => attach(undef, { ids => undef }),
+      error => 'a ids argument',
+      test  => 'Failing to pass the "ids" param fails',
+    },
+    { user  => 'unprivileged',
+      args  => attach(INVALID_BUG_ID),
+      error => "not a valid bug number",
+      test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
+    },
+    { user  => 'unprivileged',
+      args  => attach(''),
+      error => "You must enter a valid bug number",
+      test  => 'Passing empty bug id returns error "Invalid Bug ID"',
+    },
+    { user  => 'unprivileged',
+      args  => attach(INVALID_BUG_ALIAS),
+      error => "nor an alias to a bug",
+      test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+    },
+
+    # Test Comment parameter
+    { user  => 'unprivileged',
+      args  => attach($public_id, { data => undef }),
+      error => 'a data argument',
+      test  => 'Failing to pass the "data" parameter fails',
+    },
+    { user  => 'unprivileged',
+      args  => attach($public_id, { data => '' }),
+      error => "The file you are trying to attach is empty",
+      test  => 'Passing empty data fails',
+    },
+    { user  => 'unprivileged',
+      args  => attach($public_id, { data => random_string(300_000) }),
+      error => "Attachments cannot be more than",
+      test  => "Passing an attachment that's too large fails",
+    },
+
+    # Test the private parameter
+    { user  => 'unprivileged',
+      args  => attach($public_id, { is_private => 1 }),
+      error => 'attachments as private',
+      test  => 'Unprivileged user cannot add a private attachment'
+    },
+
+    # Content-type
+    { user  => 'unprivileged',
+      args  => attach($public_id, { content_type => 'foo/bar' }),
+      error => "Valid types must be of the form",
+      test  => "Well-formed but invalid content type fails",
+    },
+    { user  => 'unprivileged',
+      args  => attach($public_id, { content_type => undef }),
+      error => 'Valid types must be of the form',
+      test  => "Failing to pass content_type fails",
+    },
+    { user  => 'unprivileged',
+      args  => attach($public_id, { content_type => '' }),
+      error => 'Valid types must be of the form',
+      test  => "Empty content type fails",
+    },
+
+    # Summary
+    { user  => 'unprivileged',
+      args  => attach($public_id, { summary => undef }),
+      error => 'You must enter a description for the attachment',
+      test  => "Failing to pass summary fails",
+    },
+    { user  => 'unprivileged',
+      args  => attach($public_id, { summary => '' }),
+      error => 'You must enter a description for the attachment',
+      test  => "Empty summary fails",
+    },
+
+    # Filename
+    { user  => 'unprivileged',
+      args  => attach($public_id, { file_name => undef }),
+      error => 'You did not specify a file to attach',
+      test  => "Failing to pass file_name fails",
+    },
+    { user  => 'unprivileged',
+      args  => attach($public_id, { file_name => '' }),
+      error => 'You did not specify a file to attach',
+      test  => "Empty file_name fails",
+    },
+
+    # Success tests
+    { user => 'unprivileged',
+      args => attach($public_id),
+      test => 'Unprivileged user can add an attachment to a public bug',
+    },
+    { user => 'unprivileged',
+      args => attach($public_id, { is_patch => 1, content_type => undef }),
+      test => 'Attaching a patch with no content type works',
+    },
+    { user => 'unprivileged',
+      args => attach($public_id, { is_patch => 1,
+                     content_type => 'application/octet-stream' }),
+      test => 'Attaching a patch with a bad content_type works',
+    },
+    { user => PRIVS_USER,
+      args => attach($private_id),
+      test => 'Privileged user can add an attachment to a private bug',
+    },
+    { user => PRIVS_USER,
+      args => attach($public_id, { is_private => 1 }),
+      test => 'Insidergroup user can add a private attachment',
+    },
+);
+
+$jsonrpc_get->bz_call_fail('Bug.add_attachment', attach($public_id),
+    'must use HTTP POST', 'add_attachment fails over GET');
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.add_attachment',
+                       post_success => \&post_success, pre_call => \&pre_call);
+}
+
+# We have to encode data manually when using JSON-RPC, else it fails.
+sub pre_call {
+    my ($t, $rpc) = @_;
+    return if !$rpc->isa('QA::RPC::JSONRPC');
+    return if !defined $t->{args}->{data};
+
+    $t->{args}->{data} = encode_base64($t->{args}->{data}, '');
+}
+
+sub post_success {
+    my ($call, $t, $rpc) = @_;
+
+    my $ids = $call->result->{ids};
+    $call = $rpc->bz_call_success("Bug.attachments", {attachment_ids => $ids});
+    my $attachments = $call->result->{attachments};
+
+    foreach my $id (keys %$attachments) {
+        my $attachment = $attachments->{$id};
+        if ($t->{args}->{is_private}) {
+            ok($attachment->{is_private},
+               $rpc->TYPE . ": Attachment $id is private");
+        }
+        else {
+            ok(!$attachment->{is_private},
+               $rpc->TYPE . ": Attachment $id is NOT private");
+        }
+
+        if ($t->{args}->{is_patch}) {
+            is($attachment->{content_type}, 'text/plain',
+               $rpc->TYPE . ": Patch $id content type is text/plain");
+        }
+        else {
+            is($attachment->{content_type}, $t->{args}->{content_type},
+               $rpc->TYPE . ": Attachment $id content type is correct");
+        }
+
+        if ($rpc->isa('QA::RPC::JSONRPC')) {
+            # We encoded data in pre_call(), so we have to restore it to its original content.
+            $t->{args}->{data} = decode_base64($t->{args}->{data});
+            $attachment->{data} = decode_base64($attachment->{data});
+        }
+        is($attachment->{data}, $t->{args}->{data},
+           $rpc->TYPE . ": Attachment $id data is correct");
+    }
+}
diff --git a/xt/webservice/bug_add_comment.t b/xt/webservice/bug_add_comment.t
new file mode 100644 (file)
index 0000000..6f234b3
--- /dev/null
@@ -0,0 +1,173 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#############################################
+# Test for xmlrpc call to Bug.add_comment() #
+#############################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use Test::More tests => 141;
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ALIAS => 'aaaaaaa12345';
+use constant PRIVS_USER => 'QA_Selenium_TEST';
+use constant TIMETRACKING_USER => 'admin';
+
+use constant TEST_COMMENT => '--- Test Comment From QA Tests ---';
+use constant TOO_LONG_COMMENT => 'a' x 100000;
+
+my @tests = (
+    # Permissions
+    { args  => { id => 'public_bug', comment => TEST_COMMENT },
+      error => 'You must log in',
+      test  => 'Logged-out user cannot comment on a public bug',
+    },
+    { args  => { id => 'private_bug', comment => TEST_COMMENT },
+      error => "You must log in",
+      test  => 'Logged-out user cannot comment on a private bug',
+    },
+    { user  => 'unprivileged',
+      args  => { id => 'private_bug', comment => TEST_COMMENT },
+      error => "not authorized to access",
+      test  => "Unprivileged user can't comment on a private bug",
+    },
+
+    # Test ID parameter
+    { user  => 'unprivileged',
+      args  => { comment => TEST_COMMENT },
+      error => 'a id argument',
+      test  => 'Failing to pass the "id" param fails',
+    },
+    { user  => 'unprivileged',
+      args  => { id => INVALID_BUG_ID, comment => TEST_COMMENT },
+      error => "not a valid bug number",
+      test  => 'Passing invalid bug id returns error "Invalid Bug ID"',
+    },
+    { user  => 'unprivileged',
+      args  => { id => '', comment => TEST_COMMENT },
+      error => "You must enter a valid bug number",
+      test  => 'Passing empty bug id param returns error "Invalid Bug ID"',
+    },
+    { user  => 'unprivileged',
+      args  => { id => INVALID_BUG_ALIAS, comment => TEST_COMMENT },
+      error => "nor an alias to a bug",
+      test  => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+    },
+
+    # Test Comment parameter
+    { user  => 'unprivileged',
+      args  => { id => 'public_bug' },
+      error => 'a comment argument',
+      test  => 'Failing to pass the "comment" parameter fails',
+    },
+    { user  => 'unprivileged',
+      args  => { id => 'public_bug', comment => '' },
+      error => "a comment argument",
+      test  => 'Passing an empty comment fails',
+    },
+    { user  => 'unprivileged',
+      args  => { id => 'public_bug', comment => ' ' },
+      error => 'a comment argument',
+      test  => 'Passing only a space for comment fails',
+    },
+    { user  => 'unprivileged',
+      args  => { id => 'public_bug', comment => " \t\n\n\r\n\r\n\r " },
+      error => 'a comment argument',
+      test  => 'Passing only whitespace (including newlines) fails',
+    },
+    { user  => 'unprivileged',
+      args  => { id => 'public_bug', comment => TOO_LONG_COMMENT },
+      error => "cannot be longer than",
+      test  => "Passing a comment that's too long fails",
+    },
+
+    # Testing the "private" parameter happens in the tests for Bug.comments
+
+    # Test work_time parameter
+    # FIXME Should be testing permissions on the work_time parameter,
+    # but we currently have no way to verify whether or not time was
+    # added to the bug, and there's no error thrown if you lack perms.
+    { user  => 'admin',
+      args  => { id => 'public_bug', comment => TEST_COMMENT,
+                 work_time => 'aaa' },
+      error => "is not a numeric value",
+      test  => "Passing a non-numeric work_time fails",
+    },
+    { user  => 'admin',
+      args  => { id => 'public_bug', comment => TEST_COMMENT,
+                 work_time => '1234567890' },
+      error => 'more than the maximum',
+      test  => 'Passing too large of a work_time fails',
+    },
+    { user  => 'admin',
+      args  => { id => 'public_bug', comment => '',
+                 work_time => '1.0' },
+      error => 'a comment argument',
+      test  => 'Passing a work_time with an empty comment fails',
+    },
+
+    # Success tests
+    { user => 'unprivileged',
+      args => { id => 'public_bug', comment => TEST_COMMENT },
+      test => 'Unprivileged user can add a comment to a public bug',
+    },
+    { user => 'unprivileged',
+      args => { id => 'public_bug', comment => " \n" . TEST_COMMENT },
+      test => 'Can add a comment to a bug where the first line is whitespace',
+    },
+    { user => 'QA_Selenium_TEST',
+      args => { id => 'private_bug', comment => TEST_COMMENT },
+      test => 'Privileged user can add a comment to a private bug',
+      check_privacy => 1,
+    },
+    { user => 'QA_Selenium_TEST',
+      args => { id => 'public_bug', comment => TEST_COMMENT,
+                is_private => 1 },
+      test => 'Insidergroup user can add a private comment',
+      check_privacy => 1,
+    },
+    { user => 'admin',
+      args => { id => 'public_bug', comment => TEST_COMMENT,
+                work_time => '1.5' },
+      test => 'Timetracking user can add work_time to a bug',
+    },
+    # FIXME Need to verify that the comment added actually has work_time.
+);
+
+$jsonrpc_get->bz_call_fail('Bug.add_comment',
+    { id => 'public_bug', comment => TEST_COMMENT },
+    'must use HTTP POST', 'add_comment fails over GET');
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.add_comment',
+                       post_success => \&post_success);
+}
+
+sub post_success {
+    my ($call, $t, $rpc) = @_;
+    return unless $t->{check_privacy};
+
+    my $comment_id = $call->result->{id};
+    my $result = $rpc->bz_call_success('Bug.comments', {comment_ids => [$comment_id]});
+    if ($t->{args}->{is_private}) {
+        ok($result->result->{comments}->{$comment_id}->{is_private},
+           $rpc->TYPE . ": Comment $comment_id is private");
+    }
+    else {
+        ok(!$result->result->{comments}->{$comment_id}->{is_private},
+           $rpc->TYPE . ": Comment $comment_id is NOT private");
+    }
+}
diff --git a/xt/webservice/bug_attachments.t b/xt/webservice/bug_attachments.t
new file mode 100644 (file)
index 0000000..d528368
--- /dev/null
@@ -0,0 +1,155 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(STANDARD_BUG_TESTS PRIVATE_BUG_USER);
+use Data::Dumper;
+use List::Util qw(first);
+use MIME::Base64;
+use Test::More tests => 313;
+my ($config, @clients) = get_rpc_clients();
+
+################
+# Bug ID Tests #
+################
+
+our %attachments;
+
+sub post_bug_success {
+    my ($call, $t) = @_;
+
+    my $bugs = $call->result->{bugs};
+    is(scalar keys %$bugs, 1, "Got exactly one bug")
+        or diag(Dumper($call->result));
+
+    my $bug_attachments = (values %$bugs)[0];
+    # Collect attachment ids
+    foreach my $alias (qw(public_bug private_bug)) {
+        foreach my $is_private (0, 1) {
+            my $find_desc = "${alias}_${is_private}";
+            my $attachment = first { $_->{summary} eq $find_desc }
+                                   reverse @$bug_attachments;
+            if ($attachment) {
+                $attachments{$find_desc} = $attachment->{id};
+            }
+        }
+    }
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS, method => 'Bug.attachments',
+                       post_success => \&post_bug_success);
+}
+
+foreach my $alias (qw(public_bug private_bug)) {
+    foreach my $is_private (0, 1) {
+        ok($attachments{"${alias}_${is_private}"},
+           "Found attachment id for ${alias}_${is_private}");
+    }
+}
+
+####################
+# Attachment Tests #
+####################
+
+my $content_file = $config->{bugzilla_path} . '/xt/config/generate_test_data.pl';
+open(my $fh, '<', $content_file) or die "$content_file: $!";
+my $content;
+{ local $/; $content = <$fh>; }
+close($fh);
+
+# Access tests for public/private stuff, and also validate that the
+# format of each return value is correct.
+
+my @tests = (
+    # Logged-out user
+    { args => { attachment_ids => [$attachments{'public_bug_0'}] },
+      test => 'Logged-out user can access public attachment on public'
+              . '  bug by id',
+    },
+    { args  => { attachment_ids => [$attachments{'public_bug_1'}] },
+      test  => 'Logged-out user cannot access private attachment on public bug',
+      error => 'Sorry, you are not authorized',
+    },
+    { args  => { attachment_ids => [$attachments{'private_bug_0'}] },
+      test  => 'Logged-out user cannot access attachments by id on private bug',
+      error => 'You are not authorized to access',
+    },
+    { args  => { attachment_ids => [$attachments{'private_bug_1'}] },
+      test  => 'Logged-out user cannot access private attachment on '
+               . ' private bug',
+      error => 'You are not authorized to access',
+    },
+
+    # Logged-in, unprivileged user.
+    { user => 'unprivileged',
+      args => { attachment_ids => [$attachments{'public_bug_0'}] },
+      test => 'Logged-in user can see a public attachment on a public bug by id',
+    },
+    { user  => 'unprivileged',
+      args  => { attachment_ids => [$attachments{'public_bug_1'}] },
+      test  => 'Logged-in user cannot access private attachment on public bug',
+      error => 'Sorry, you are not authorized',
+    },
+    { user  => 'unprivileged',
+      args  => { attachment_ids => [$attachments{'private_bug_0'}] },
+      test  => 'Logged-in user cannot access attachments by id on private bug',
+      error => "You are not authorized to access",
+    },
+    { user  => 'unprivileged',
+      args  => { attachment_ids => [$attachments{'private_bug_1'}] },
+      test  => 'Logged-in user cannot access private attachment on private bug',
+      error => "You are not authorized to access",
+    },
+
+    # User who can see private bugs and private attachments
+    { user => PRIVATE_BUG_USER,
+      args => { attachment_ids => [$attachments{'public_bug_1'}] },
+      test => PRIVATE_BUG_USER . ' can see private attachment on public bug',
+    },
+    { user  => PRIVATE_BUG_USER,
+      args  => { attachment_ids => [$attachments{'private_bug_1'}] },
+      test  => PRIVATE_BUG_USER . ' can see private attachment on private bug',
+    },
+);
+
+sub post_success {
+    my ($call, $t, $rpc) = @_;
+    is(scalar keys %{ $call->result->{attachments} }, 1,
+       "Got exactly one attachment");
+    my $attachment = (values %{ $call->result->{attachments} })[0];
+
+    cmp_ok($attachment->{last_change_time}, '=~', $rpc->DATETIME_REGEX,
+           "last_change_time is in the right format");
+    cmp_ok($attachment->{creation_time}, '=~', $rpc->DATETIME_REGEX,
+           "creation_time is in the right format");
+    is($attachment->{is_obsolete}, 0, 'is_obsolete is 0');
+    cmp_ok($attachment->{bug_id}, '=~', qr/^\d+$/, "bug_id is an integer");
+    cmp_ok($attachment->{id}, '=~', qr/^\d+$/, "id is an integer");
+    is($attachment->{content_type}, 'application/x-perl',
+       "content_type is correct");
+    cmp_ok($attachment->{file_name}, '=~', qr/^\w+\.pl$/,
+           "filename is in the expected format");
+    is($attachment->{creator}, $config->{QA_Selenium_TEST_user_login},
+       "creator is the correct user");
+    my $data = $attachment->{data};
+    $data = decode_base64($data) if $rpc->isa('QA::RPC::JSONRPC');
+    is($data, $content, 'data is correct');
+    is($attachment->{size}, length($data), "size matches data's size");
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(method => 'Bug.attachments', tests => \@tests,
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_comments.t b/xt/webservice/bug_comments.t
new file mode 100644 (file)
index 0000000..d66e445
--- /dev/null
@@ -0,0 +1,178 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##########################################
+# Test for xmlrpc call to Bug.comments() #
+##########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use DateTime;
+use QA::Util;
+use QA::Tests qw(STANDARD_BUG_TESTS PRIVATE_BUG_USER);
+use Test::More tests => 331;
+my ($config, @clients) = get_rpc_clients();
+
+# These gets populated when we call Bug.add_comment.
+our $creation_time;
+our %comments = (
+    public_comment_public_bug  => 0,
+    public_comment_private_bug  => 0,
+    private_comment_public_bug  => 0,
+    private_comment_private_bug => 0,
+);
+
+sub test_comments {
+    my ($comments_returned, $call, $t, $rpc) = @_;
+
+    my $comment = $comments_returned->[0];
+    ok($comment->{bug_id}, "bug_id exists");
+    # FIXME At some point we should test attachment_id here.
+
+    if ($t->{args}->{comment_ids}) {
+        my $expected_id = $t->{args}->{comment_ids}->[0];
+        is($comment->{id}, $expected_id, "comment id is correct");
+
+        my %reverse_map = reverse %comments;
+        my $expected_text = $reverse_map{$expected_id};
+        is($comment->{text}, $expected_text, "comment has the correct text");
+
+        my $priv_login = $rpc->bz_config->{PRIVATE_BUG_USER . '_user_login'};
+        is($comment->{creator}, $priv_login, "comment creator is correct");
+
+        my $creation_day;
+        if ($rpc->isa('QA::RPC::XMLRPC')) {
+            $creation_day = $creation_time->ymd('');
+        }
+        else {
+            $creation_day = $creation_time->ymd;
+        }
+        like($comment->{time}, qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
+             "comment time has the right format");
+    }
+    else {
+        foreach my $field (qw(id text creator time)) {
+            ok(defined $comment->{$field}, "$field is defined");
+        }
+    }
+}
+
+################
+# Bug ID Tests #
+################
+
+sub post_bug_success {
+    my ($call, $t) = @_;
+    my @bugs = values %{ $call->result->{bugs} };
+    is(scalar @bugs, 1, "Got exactly one bug");
+    my @comments = map { @{ $_->{comments} } } @bugs;
+    test_comments(\@comments, @_);
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS, method => 'Bug.comments',
+                       post_success => \&post_bug_success);
+}
+
+####################
+# Comment ID Tests #
+####################
+
+# First, create comments using add_comment.
+my @add_comment_tests;
+foreach my $key (keys %comments) {
+    $key =~ /^([a-z]+)_comment_(\w+)$/;
+    my $is_private = ($1 eq 'private' ? 1 : 0);
+    my $bug_alias = $2;
+    push(@add_comment_tests, { args => { id => $bug_alias, comment => $key,
+                                         private => $is_private },
+                               test => "Add comment: $key",
+                               user => PRIVATE_BUG_USER });
+}
+
+# Set the comment id for each comment that we add, so we can test getting
+# them back, later.
+sub post_add {
+    my ($call, $t) = @_;
+    my $key = $t->{args}->{comment};
+    $comments{$key} = $call->result->{id};
+}
+
+$creation_time = DateTime->now();
+# We only need to create these comments once, with one of the interfaces.
+$clients[0]->bz_run_tests(
+    tests => \@add_comment_tests, method => 'Bug.add_comment',
+    post_success => \&post_add);
+
+# Now check access on each private and public comment
+
+my @comment_tests = (
+    # Logged-out user
+    { args => { comment_ids => [$comments{'public_comment_public_bug'}] },
+      test => 'Logged-out user can access public comment on public bug by id',
+    },
+    { args  => { comment_ids => [$comments{'private_comment_public_bug'}] },
+      test  => 'Logged-out user cannot access private comment on public bug',
+      error => 'is private',
+    },
+    { args  => { comment_ids => [$comments{'public_comment_private_bug'}] },
+      test  => 'Logged-out user cannot access comments by id on private bug',
+      error => 'You are not authorized to access',
+    },
+    { args  => { comment_ids => [$comments{'private_comment_private_bug'}] },
+      test  => 'Logged-out user cannot access private comment on private bug',
+      error => 'You are not authorized to access',
+    },
+
+    # Logged-in, unprivileged user.
+    { user => 'unprivileged',
+      args => { comment_ids => [$comments{'public_comment_public_bug'}] },
+      test => 'Logged-in user can see a public comment on a public bug by id',
+    },
+    { user  => 'unprivileged',
+      args  => { comment_ids => [$comments{'private_comment_public_bug'}] },
+      test  => 'Logged-in user cannot access private comment on public bug',
+      error => 'is private',
+    },
+    { user  => 'unprivileged',
+      args  => { comment_ids => [$comments{'public_comment_private_bug'}] },
+      test  => 'Logged-in user cannot access comments by id on private bug',
+      error => "You are not authorized to access",
+    },
+    { user  => 'unprivileged',
+      args  => { comment_ids => [$comments{'private_comment_private_bug'}] },
+      test  => 'Logged-in user cannot access private comment on private bug',
+      error => "You are not authorized to access",
+    },
+
+    # User who can see private bugs and private comments
+    { user => PRIVATE_BUG_USER,
+      args => { comment_ids => [$comments{'private_comment_public_bug'}] },
+      test => PRIVATE_BUG_USER . ' can see private comment on public bug',
+    },
+    { user  => PRIVATE_BUG_USER,
+      args  => { comment_ids => [$comments{'private_comment_private_bug'}] },
+      test  => PRIVATE_BUG_USER . ' can see private comment on private bug',
+    },
+);
+
+sub post_comments {
+    my ($call) = @_;
+    my @comments = values %{ $call->result->{comments} };
+    is(scalar @comments, 1, "Got exactly one comment");
+    test_comments(\@comments, @_);
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => \@comment_tests, method => 'Bug.comments',
+                       post_success => \&post_comments);
+}
diff --git a/xt/webservice/bug_create.t b/xt/webservice/bug_create.t
new file mode 100644 (file)
index 0000000..6d7c8e1
--- /dev/null
@@ -0,0 +1,243 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+########################################
+# Test for xmlrpc call to Bug.create() #
+########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Storable qw(dclone);
+use Test::More tests => 293;
+use QA::Util;
+use QA::Tests qw(create_bug_fields PRIVATE_BUG_USER);
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+########################
+# Bug.create() testing #
+########################
+
+my $bug_fields = create_bug_fields($config);
+
+# hash to contain all the possible $bug_fields values that
+# can be passed to createBug()
+my $fields = {
+    summary => {
+        undefined => {
+            faultstring => 'You must enter a summary for this bug',
+            value       => undef
+        },
+    },
+
+    product => {
+        undefined => { faultstring => 'You must select/enter a product.', value => undef },
+        invalid =>
+            { faultstring => 'does not exist', value => 'does-not-exist' },
+    },
+
+    component => {
+        undefined => {
+            faultstring => 'you must first choose a component',
+            value       => undef
+        },
+        invalid => {
+            faultstring => "There is no component named 'does-not-exist'",
+            value => 'does-not-exist'
+        },
+    },
+
+    version => {
+        undefined =>
+            { faultstring => 'You must select/enter a version.', value => undef },
+        invalid => {
+            faultstring => "There is no version named 'does-not-exist' in the",
+            value       => 'does-not-exist'
+        },
+    },
+    platform => {
+        undefined =>
+            { faultstring => 'You must select/enter a Hardware.',
+              value => '' },
+        invalid => {
+            faultstring => "There is no Hardware named 'does-not-exist'.",
+            value       => 'does-not-exist'
+        },
+    },
+
+    status => {
+        invalid => {
+            faultstring => "There is no status named 'does-not-exist'",
+            value       => 'does-not-exist'
+        },
+    },
+
+    severity => {
+        undefined =>
+            { faultstring => 'You must select/enter a Severity.',
+              value => '' },
+        invalid => {
+            faultstring => "There is no Severity named 'does-not-exist'.",
+            value       => 'does-not-exist'
+        },
+    },
+
+    priority => {
+        undefined =>
+            { faultstring => 'You must select/enter a Priority.',
+              value => '' },
+        invalid => {
+            faultstring => "There is no Priority named 'does-not-exist'.",
+            value       => 'does-not-exist'
+        },
+    },
+
+    op_sys => {
+        undefined => {
+            faultstring => 'You must select/enter a OS.',
+            value       => ''
+        },
+        invalid => {
+            faultstring => "There is no OS named 'does-not-exist'.",
+            value       => 'does-not-exist'
+        },
+    },
+
+    cc => {
+        invalid => {
+            faultstring => 'not a valid username',
+            value       => ['nonuserATbugillaDOTorg']
+        },
+    },
+
+    assigned_to => {
+        invalid => {
+            faultstring => "There is no user named 'does-not-exist'",
+            value       => 'does-not-exist'
+        },
+    },
+    qa_contact => {
+        invalid => {
+            faultstring => "There is no user named 'does-not-exist'",
+            value       => 'does-not-exist'
+        },
+    },
+    alias => {
+        long => {
+            faultstring => 'Bug aliases cannot be longer than 40 characters',
+            value       => 'MyyyyyyyyyyyyyyyyyyBugggggggggggggggggggggg'
+        },
+        existing => {
+            faultstring => 'already taken the alias',
+            value       => 'public_bug'
+        },
+        numeric => {
+            faultstring => 'aliases cannot be merely numbers',
+            value       => '12345'
+        },
+        commma_or_space_separated => {
+            faultstring => 'contains one or more commas or spaces',
+            value       => ['Bug 12345']
+        },
+
+    },
+    groups => {
+        non_existent => {
+            faultstring => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+            value => [random_string(20)],
+        },
+    },
+    comment_is_private => {
+        invalid => {
+             faultstring => 'you are not allowed to.+comments.+private',
+             value => 1,
+        }
+    },
+};
+
+$jsonrpc_get->bz_call_fail('Bug.create', $bug_fields,
+    'must use HTTP POST', 'create fails over GET');
+
+my @tests = (
+    { args  => $bug_fields,
+      error => "You must log in",
+      test  => "Cannot file bugs as a logged-out user",
+    },
+    { user => PRIVATE_BUG_USER,
+      args => { %$bug_fields, product => 'QA-Selenium-TEST',
+                component => 'QA-Selenium-TEST',
+                target_milestone => 'QAMilestone',
+                version => 'QAVersion',
+                groups => ['QA-Selenium-TEST'],
+                # These are set here because we can't actually set them,
+                # and we need the values to be correct for post_success.
+                qa_contact => $config->{PRIVATE_BUG_USER . '_user_login'},
+                status => 'UNCONFIRMED' },
+      test => "Authorized user can file a bug against a group",
+    },
+    { user => PRIVATE_BUG_USER,
+      args => { %$bug_fields, comment_is_private => 1,
+                # These are here because PRIVATE_BUG_USER can't set them
+                # and we need their values to be correct for post_success.
+                assigned_to => $config->{'permanent_user'},
+                qa_contact => '',
+                status => 'UNCONFIRMED' },
+      test => "Insider can create a private description"
+    },
+    { user => 'editbugs',
+      args => $bug_fields,
+      test => "Creating a bug with standard values succeeds",
+    },
+);
+
+# Convert the $fields tests into standard bz_run_tests format.
+foreach my $field (sort keys %$fields) {
+    my $test_values = $fields->{$field};
+    foreach my $test_name (sort keys %$test_values) {
+        my $input_fields = dclone($bug_fields);
+        my $check_value = $test_values->{$test_name}->{value};
+        my $error       = $test_values->{$test_name}->{faultstring};
+        $input_fields->{$field} = $check_value;
+        my $test = { user => 'editbugs', args => $input_fields,
+                     error => $error,
+                     test => "$field $test_name: fails as expected" };
+        push(@tests, $test);
+    }
+}
+
+sub post_success {
+    my ($call, $t, $rpc) = @_;
+
+    my $id = $call->result->{id};
+    ok($id, $rpc->TYPE . ": Result has an id: $id");
+
+    my $get_call = $rpc->bz_call_success('Bug.get', { ids => [$id] });
+    my $bug = $get_call->result->{bugs}->[0];
+
+    my $expect = dclone $t->{args};
+
+    my $comment_is_private = delete $expect->{comment_is_private};
+    $expect->{creator} = $rpc->bz_config->{$t->{user} . '_user_login'};
+
+    my @fields = keys %$expect;
+    $rpc->bz_test_bug(\@fields, $bug, $expect, $t);
+
+    my $comment_call = $rpc->bz_call_success('Bug.comments', { ids => [$id] });
+    my $comment = $comment_call->result->{bugs}->{$id}->{comments}->[0];
+    is($comment->{is_private} ? 1 : 0, $comment_is_private ? 1 : 0,
+       $rpc->TYPE . ": comment privacy is correct");
+}
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.create',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_fields.t b/xt/webservice/bug_fields.t
new file mode 100644 (file)
index 0000000..097a607
--- /dev/null
@@ -0,0 +1,223 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use Test::More;
+use List::Util qw(first);
+use QA::Util;
+
+my ($config, @clients) = get_rpc_clients();
+plan tests => ($config->{test_extensions} ? 1338 : 1320);
+
+use constant INVALID_FIELD_NAME => 'invalid_field';
+use constant INVALID_FIELD_ID => -1;
+sub GLOBAL_GENERAL_FIELDS {
+    my @fields = qw(
+        attach_data.thedata
+        attachments.description
+        attachments.filename
+        attachments.isobsolete
+        attachments.ispatch
+        attachments.isprivate
+        attachments.mimetype
+        attachments.submitter
+
+        flagtypes.name
+        requestees.login_name
+        setters.login_name
+
+        alias
+        assigned_to
+        blocked
+        bug_file_loc
+        bug_group
+        bug_id
+        cc
+        cclist_accessible
+        classification
+        commenter
+        content
+        creation_ts
+        days_elapsed
+        delta_ts
+        dependson
+        everconfirmed
+        keywords
+        longdesc
+        longdescs.isprivate
+        owner_idle_time
+        product
+        qa_contact
+        reporter
+        reporter_accessible
+        see_also
+        short_desc
+        status_whiteboard
+
+        deadline
+        estimated_time
+        percentage_complete
+        remaining_time
+        work_time
+    );
+    push(@fields, 'votes') if QA::Util::get_config()->{test_extensions};
+
+    return @fields;
+}
+
+use constant STANDARD_SELECT_FIELDS =>
+    qw(bug_severity bug_status op_sys priority rep_platform resolution);
+
+use constant ALL_SELECT_FIELDS => (STANDARD_SELECT_FIELDS,
+    qw(cf_qa_status cf_single_select));
+use constant PRODUCT_FIELDS => qw(version target_milestone component);
+use constant ALL_FIELDS => (GLOBAL_GENERAL_FIELDS, ALL_SELECT_FIELDS,
+                            PRODUCT_FIELDS);
+use constant MANDATORY_FIELDS => qw(short_desc product version component);
+
+use constant PUBLIC_PRODUCT  => 'Another Product';
+use constant PRIVATE_PRODUCT => 'QA-Selenium-TEST';
+
+sub get_field {
+    my ($fields, $field) = @_;
+    return first { $_->{name} eq $field } @$fields;
+}
+
+sub get_products_from_field {
+    my $field = shift;
+    my %products;
+    foreach my $value (@{ $field->{values} }) {
+        foreach my $vis_value (@{ $value->{visibility_values} }) {
+            $products{$vis_value} = 1;
+        }
+    }
+    return \%products;
+}
+
+our %field_ids;
+foreach my $rpc (@clients) {
+    my $call = $rpc->bz_call_success('Bug.fields');
+    my $fields = $call->result->{fields};
+    foreach my $field (ALL_FIELDS) {
+        my $field_data = get_field($fields, $field);
+        ok($field_data, "$field is in the returned result")
+            or diag(Dumper($fields));
+        $field_ids{$field} = $field_data->{id};
+
+        if (grep($_ eq $field, MANDATORY_FIELDS)) {
+            ok($field_data->{is_mandatory}, "$field is mandatory");
+        }
+        else {
+            ok(!$field_data->{is_mandatory}, "$field is not mandatory");
+        }
+    }
+
+    foreach my $field (ALL_SELECT_FIELDS, PRODUCT_FIELDS) {
+        my $field_data = get_field($fields, $field);
+        ok(defined $field_data->{visibility_values},
+           "$field has visibility_values defined");
+        my $field_vis_undefs = grep { !defined $_ }
+                                    @{ $field_data->{visibility_values} };
+        is($field_vis_undefs, 0, "$field.visibility_values has no undefs")
+          or diag(Dumper($field_data->{visibility_values}));
+
+        ok(defined $field_data->{values},
+           "$field has 'values' defined");
+        my $num_values = scalar @{ $field_data->{values} };
+        ok($num_values, "$field has $num_values values");
+        # The first bug status is a fake one and has no name, so we choose the 2nd item.
+        my $first_value = $field_data->{values}->[1];
+        ok(defined $first_value->{name}, 'The first value has a name')
+            or diag(Dumper($field_data->{values}));
+        # The sortkey for milestones can be negative.
+        cmp_ok($first_value->{sortkey}, '=~', qr/^-?\d+$/,
+               "The first value has a numeric sortkey");
+
+        ok(defined $first_value->{visibility_values},
+           "$field has visibilty_values defined on its first value")
+            or diag(Dumper($field_data->{values}));
+        my @value_visibility_values = map { @{ $_->{visibility_values} } }
+                                      @{ $field_data->{values} };
+        my $undefs = grep { !defined $_ } @value_visibility_values;
+        is($undefs, 0,
+           "$field.values.visibility_values has no undefs");
+    }
+
+    foreach my $field (PRODUCT_FIELDS) {
+        my $field_data = get_field($fields, $field);
+        is($field_data->{value_field}, 'product',
+           "The value_field for $field is 'product'");
+        my $products = get_products_from_field($field_data);
+        ok($products->{+PUBLIC_PRODUCT},
+           "$field values are returned for the public product");
+        ok(!$products->{+PRIVATE_PRODUCT},
+           "No $field values are returned for the private product");
+    }
+}
+
+my @all_tests = (
+    { args => { ids   => [values %field_ids],
+                names => [ALL_FIELDS] },
+                test => 'Getting all fields by name and id simultaneously',
+                count => scalar ALL_FIELDS
+    },
+    { args  => { names => [INVALID_FIELD_NAME] },
+      error => "There is no field named",
+      test  => 'Invalid field name'
+    },
+    { args  => { ids => [INVALID_FIELD_ID] },
+      error => 'must be numeric',
+      test  => 'Invalid field id'
+    },
+    { user  => 'QA_Selenium_TEST',
+      args  => { names => [PRODUCT_FIELDS] },
+      test  => 'Getting product-specific fields as a privileged user',
+      count => scalar PRODUCT_FIELDS,
+      product_private_values => 1
+    },
+);
+
+foreach my $field (ALL_FIELDS) {
+    push(@all_tests,
+         { args => { names => [$field] },
+           test => "Logged-out users can get the $field field by name" });
+    push(@all_tests,
+         { args => { ids => [$field_ids{$field}] },
+           test => "Logged-out users can get the $field by id" });
+}
+
+sub post_success {
+    my ($call, $t) = @_;
+    my $fields = $call->result->{fields};
+    my $count = $t->{count};
+    $count = 1 if !defined $count;
+    is(scalar @$fields, $count, "Exactly $count field(s) returned");
+
+    if ($t->{product_private_values}) {
+        foreach my $field (@$fields) {
+            my $name = $field->{name};
+            my $field_data = get_field($fields, $name);
+            my $products = get_products_from_field($field_data);
+            ok($products->{+PUBLIC_PRODUCT},
+               "$name values are returned for the public product");
+            ok($products->{+PRIVATE_PRODUCT},
+               "$name values are returned for the private product");
+        }
+    }
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => \@all_tests,  method => 'Bug.fields',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_get.t b/xt/webservice/bug_get.t
new file mode 100644 (file)
index 0000000..e05fe2c
--- /dev/null
@@ -0,0 +1,150 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+###########################################
+# Test for xmlrpc call to Bug.get()       #
+###########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use DateTime;
+use QA::Util;
+use QA::Tests qw(bug_tests PRIVATE_BUG_USER);
+use Test::More tests => 988;
+my ($config, @clients) = get_rpc_clients();
+
+my $xmlrpc = $clients[0];
+our $creation_time = DateTime->now();
+our ($public_bug, $private_bug) = $xmlrpc->bz_create_test_bugs('private');
+my $private_id = $private_bug->{id};
+my $public_id = $public_bug->{id};
+
+my $base_url = $config->{browser_url} . "/"
+              . $config->{bugzilla_installation} . '/';
+
+# Set a few fields on the private bug, including setting up
+# a dependency relationship.
+$xmlrpc->bz_log_in(PRIVATE_BUG_USER);
+$xmlrpc->bz_call_success('Bug.update', {
+    ids => [$private_id],
+    blocks => { set => [$public_id] },
+    dupe_of => $public_id,
+    is_creator_accessible => 0,
+    keywords => { set => ['test-keyword-1', 'test-keyword-2'] },
+    see_also => { add => ["${base_url}show_bug.cgi?id=$public_id",
+                          "http://landfill.bugzilla.org/show_bug.cgi?id=123456"] },
+    cf_qa_status => ['in progress', 'verified'],
+    cf_single_select => 'two',
+}, 'Update the private bug');
+$xmlrpc->bz_call_success('User.logout');
+
+$private_bug->{blocks} = [$public_id];
+$private_bug->{dupe_of} = $public_id;
+$private_bug->{status} = 'RESOLVED';
+$private_bug->{is_open} = 0;
+$private_bug->{resolution} = 'DUPLICATE';
+$private_bug->{is_creator_accessible} = 0;
+$private_bug->{is_cc_accessible} = 1;
+$private_bug->{keywords} = ['test-keyword-1', 'test-keyword-2'];
+$private_bug->{see_also} = ["${base_url}show_bug.cgi?id=$public_id",
+                            "http://landfill.bugzilla.org/show_bug.cgi?id=123456"];
+$private_bug->{cf_qa_status} = ['in progress', 'verified'];
+$private_bug->{cf_single_select} = 'two';
+
+$public_bug->{depends_on} = [$private_id];
+$public_bug->{dupe_of} = undef;
+$public_bug->{resolution} = '';
+$public_bug->{is_open} = 1;
+$public_bug->{is_creator_accessible} = 1;
+$public_bug->{is_cc_accessible} = 1;
+$public_bug->{keywords} = [];
+# Local Bugzilla bugs are automatically updated.
+$public_bug->{see_also} = ["${base_url}show_bug.cgi?id=$private_id"];
+$public_bug->{cf_qa_status} = [];
+$public_bug->{cf_single_select} = '---';
+
+# Fill in the timetracking fields on the public bug.
+$xmlrpc->bz_log_in('admin');
+$xmlrpc->bz_call_success('Bug.update', {
+    ids => [$public_id],
+    deadline => '2038-01-01',
+    estimated_time => '10.0',
+    remaining_time => '5.0',
+});
+$xmlrpc->bz_call_success('User.logout');
+
+# Populate other fields.
+$public_bug->{classification} = 'Unclassified';
+$private_bug->{classification} = 'Unclassified';
+$private_bug->{groups} = ['QA-Selenium-TEST'];
+$public_bug->{groups} = [];
+
+# The user filing $private_bug doesn't have permission to set the status
+# or qa_contact, so they differ from normal $public_bug values.
+$private_bug->{qa_contact} = $config->{PRIVATE_BUG_USER . '_user_login'};
+
+sub post_success {
+    my ($call, $t, $rpc) = @_;
+
+    is(scalar @{ $call->result->{bugs} }, 1, "Got exactly one bug");
+    my $bug = $call->result->{bugs}->[0];
+
+    if ($t->{user} && $t->{user} eq 'admin') {
+        ok(exists $bug->{estimated_time} && exists $bug->{remaining_time},
+           'Admin correctly gets time-tracking fields');
+        is($bug->{deadline}, '2038-01-01', 'deadline is correct');
+        cmp_ok($bug->{estimated_time}, '==', '10.0',
+               'estimated_time is correct');
+        cmp_ok($bug->{remaining_time}, '==', '5.0',
+               'remaining_time is correct');
+    }
+    else {
+        ok(!exists $bug->{estimated_time} && !exists $bug->{remaining_time},
+           'Time-tracking fields are not returned to non-privileged users');
+    }
+
+    if ($t->{user}) {
+        ok($bug->{update_token}, 'Update token returned for logged-in user');
+    }
+    else {
+        ok(!exists $bug->{update_token},
+           'Update token not returned for logged-out users');
+    }
+
+    my $expect = $bug->{id} == $private_bug->{id} ? $private_bug : $public_bug;
+
+    my @fields = sort keys %$expect;
+    push(@fields, 'creation_time', 'last_change_time');
+
+    $rpc->bz_test_bug(\@fields, $bug, $expect, $t, $creation_time);
+}
+
+my @tests = (
+    @{ bug_tests($public_id, $private_id) },
+    { args => { ids => [$public_id],
+                include_fields => ['id', 'summary', 'groups'] },
+      test => 'include_fields',
+    },
+    { args => { ids => [$public_id],
+                exclude_fields => ['assigned_to', 'cf_qa_status'] },
+      test => 'exclude_fields' },
+    { args => { ids => [$public_id],
+                include_fields => ['id', 'summary', 'groups'],
+                exclude_fields => ['summary'] },
+      test => 'exclude_fields overrides include_fields' },
+);
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => \@tests,  method => 'Bug.get',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_history.t b/xt/webservice/bug_history.t
new file mode 100644 (file)
index 0000000..02ec1c1
--- /dev/null
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#########################################
+# Test for xmlrpc call to Bug.history() #
+#########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(STANDARD_BUG_TESTS);
+use Test::More tests => 114;
+my ($config, @clients) = get_rpc_clients();
+
+sub post_success {
+    my ($call, $t) = @_;
+    is(scalar @{ $call->result->{bugs} }, 1, "Got exactly one bug");
+    isa_ok($call->result->{bugs}->[0]->{history}, 'ARRAY', "Bug's history");
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS,
+                       method => 'Bug.history', post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_legal_values.t b/xt/webservice/bug_legal_values.t
new file mode 100644 (file)
index 0000000..2f775e5
--- /dev/null
@@ -0,0 +1,104 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##############################################
+# Test for xmlrpc call to Bug.legal_values() #
+##############################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 269;
+use QA::Util;
+my ($config, @clients) = get_rpc_clients();
+
+use constant INVALID_PRODUCT_ID => -1;
+use constant INVALID_FIELD_NAME => 'invalid_field';
+use constant GLOBAL_FIELDS =>
+    qw(bug_severity bug_status op_sys priority rep_platform resolution
+       cf_qa_status cf_single_select);
+use constant PRODUCT_FIELDS => qw(version target_milestone component);
+
+
+my $products = $clients[0]->bz_get_products();
+my $public_product = $products->{'Another Product'};
+my $private_product = $products->{'QA-Selenium-TEST'};
+
+my @all_tests;
+
+for my $field (GLOBAL_FIELDS) {
+    push(@all_tests,
+         { args => { field => $field },
+           test => "Logged-out user can get $field values" });
+}
+
+for my $field (PRODUCT_FIELDS) {
+    my @tests = (
+        { args  => { field => $field },
+          error => "argument was not set",
+          test  => "$field can't be accessed without a value for 'product'",
+        },
+        { args  => { product_id => INVALID_PRODUCT_ID, field => $field },
+          error => "does not exist",
+          test  => "$field cannot be accessed with an invalid product id",
+        },
+
+        { args  => { product_id => $private_product, field => $field },
+          error => "you don't have access",
+          test => "Logged-out user cannot access $field in private product"
+        },
+        { args  => { product_id => $public_product, field => $field },
+          test  => "Logged-out user can access $field in a public product",
+        },
+
+        { user  => 'unprivileged',
+          args  => { product_id => $private_product, field => $field },
+          error => "you don't have access",
+          test  => "Unprivileged user cannot access $field in private product",
+        },
+        { user => 'unprivileged',
+          args => { product_id => $public_product, field => $field },
+          test => "Logged-in user can access $field in public product",
+        },
+
+        { user => 'QA_Selenium_TEST',
+          args => { product_id => $private_product, field  => $field },
+          test => "Privileged user can access $field in a private product",
+        },
+    );
+
+    push(@all_tests, @tests);
+}
+
+my @extra_tests = (
+    { args  => { product_id => $private_product, },
+      error => "requires a field argument",
+      test  =>  "Passing product_id without 'field' throws an error",
+    },
+    { args  => { field => INVALID_FIELD_NAME },
+      error => "Can't use \"" . INVALID_FIELD_NAME . "\" as a field name",
+      test  => 'Invalid field name'
+    },
+);
+
+push(@all_tests, @extra_tests);
+
+sub post_success {
+    my ($call) = @_;
+
+    cmp_ok(scalar @{ $call->result->{'values'} }, '>', 0,
+           'Got one or more values');
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => \@all_tests,  method => 'Bug.legal_values',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_search.t b/xt/webservice/bug_search.t
new file mode 100644 (file)
index 0000000..93a517e
--- /dev/null
@@ -0,0 +1,211 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+########################################
+# Test for xmlrpc call to Bug.search() #
+########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER);
+use DateTime;
+use List::MoreUtils qw(uniq);
+use Test::More;
+use Data::Dumper;
+
+my ($config, @clients) = get_rpc_clients();
+plan tests => $config->{test_extensions} ? 531 : 522;
+
+my ($public_bug, $private_bug) = $clients[0]->bz_create_test_bugs('private');
+
+# Add aliases to both bugs
+$public_bug->{alias}  = random_string(40);
+$private_bug->{alias} = random_string(40);
+my $alias_tests = [
+    { user => 'editbugs',
+      args => { ids => [ $public_bug->{id} ], alias => $public_bug->{alias} },
+      test => 'Add alias to public bug' },
+    { user => PRIVATE_BUG_USER,
+      args => { ids => [ $private_bug->{id} ],
+                cc  => { add => [ $config->{'editbugs_user_login'} ] } },
+      test => 'Add editusers to cc of private bug' },
+    { user => 'editbugs',
+      args => { ids => [ $private_bug->{id} ], alias => $private_bug->{alias} },
+      test => 'Add alias to private bug' },
+    { user => PRIVATE_BUG_USER,
+      args => { ids => [ $private_bug->{id} ],
+                cc  => { remove => [ $config->{'editbugs_user_login'} ] } },
+      test => 'Remove editusers from cc of private bug' },
+];
+$clients[0]->bz_run_tests(tests => $alias_tests, method => 'Bug.update');
+
+my @tests;
+foreach my $field (keys %$public_bug) {
+    next if ($field eq 'cc' or $field eq 'description');
+    my $test = { args => { $field => $public_bug->{$field} },
+                 test => "Search by $field" };
+    if ( grep($_ eq $field, qw(alias whiteboard summary)) ) {
+        $test->{exactly} = 1; $test->{bugs} = 1;
+    }
+    push(@tests, $test);
+}
+
+push(@tests, (
+    { args  => { offset => 1 },
+      test  => "Offset without limit fails",
+      error => 'requires a limit argument',
+    },
+
+    { args => { alias => $private_bug->{alias} },
+      test => 'Logged-out cannot find a private_bug by alias',
+      bugs => 0,
+    },
+
+    { args => { creation_time => '19700101T00:00:00' },
+      test => 'Get all bugs by creation time',
+    },
+    { args => { creation_time => '20380101T00:00:00' },
+      test => 'Get no bugs, by creation time',
+      bugs => 0,
+    },
+    { args => { last_change_time => '19700101T00:00:00' },
+      test => 'Get all bugs by last_change_time',
+    },
+    { args => { last_change_time => '20380101T00:00:00' },
+      test => 'Get no bugs by last_change_time',
+      bugs => 0,
+    },
+
+    { args => { reporter => $config->{editbugs_user_login} },
+      test => 'Search by reporter',
+    },
+    { args => { resolution => '' },
+      test => 'Search for empty resolution',
+    },
+    { args => { resolution => 'NO_SUCH_RESOLUTION' },
+      test => 'Search for invalid resolution',
+      bugs => 0,
+    },
+    { args => { summary => substr($public_bug->{summary}, 0, 50) },
+      test => 'Search by partial summary',
+      bugs => 1, exactly => 1
+    },
+    { args => { summary => random_string() . ' ' . random_string() },
+      test => 'Summary search that returns no results',
+      bugs => 0,
+    },
+    { args => { summary => [split(/\s/, $public_bug->{summary})] },
+      test => 'Summary search using multiple terms',
+    },
+
+    { args => { whiteboard => substr($public_bug->{whiteboard}, 0, 50) },
+      test => 'Search by partial whiteboard',
+      bugs => 1, exactly => 1,
+    },
+    { args => { whiteboard => random_string(100) },
+      test => 'Whiteboard search that returns no results',
+      bugs => 0,
+    },
+    { args => { whiteboard => [split(/\s/, $public_bug->{whiteboard})] },
+      test => 'Whiteboard search using multiple terms',
+      bugs => 1, exactly => 1,
+    },
+
+    { args => { product => $public_bug->{product},
+                component => $public_bug->{component},
+                last_change_time => '19700101T00:00:00' },
+      test => 'Search by multiple arguments',
+    },
+
+    # Logged-in user who can see private bugs
+    { user => PRIVATE_BUG_USER,
+      args => { alias => [$public_bug->{alias}, $private_bug->{alias}] },
+      test => 'Search using two aliases (including one private)',
+      bugs => 2, exactly => 1,
+    },
+    { user => PRIVATE_BUG_USER,
+      args => { product => [$public_bug->{product}, $private_bug->{product}],
+                limit => 1 },
+      test => 'Limit 1',
+      bugs => 1, exactly => 1,
+    },
+    { user => PRIVATE_BUG_USER,
+      args => { product => [$public_bug->{product}, $private_bug->{product}],
+                limit => 1, offset => 1 },
+      test => 'Limit 1 Offset 1',
+      bugs => 1, exactly => 1,
+    },
+
+    # include_fields ane exclude_fields
+    { args => { id => $public_bug->{id},
+                include_fields => ['id', 'alias', 'summary', 'groups'] },
+      test => 'include_fields',
+    },
+    { args => { id => $public_bug->{id},
+                exclude_fields => ['assigned_to', 'cf_qa_status'] },
+      test => 'exclude_fields' },
+    { args => { id => $public_bug->{id},
+                include_fields => ['id', 'alias', 'summary', 'groups'],
+                exclude_fields => ['summary'] },
+      test => 'exclude_fields overrides include_fields' },
+));
+
+push(@tests,
+    { args => { votes => 1 },
+      test => 'Search by votes',
+      bugs => -1, # We don't care how many it returns, for now.
+    }) if $config->{test_extensions};
+
+sub post_success {
+    my ($call, $t) = @_;
+    my $bugs = $call->result->{bugs};
+
+    my $expected_count = $t->{bugs};
+    $expected_count = 1 if !defined $expected_count;
+    if ($expected_count) {
+        my $operator = $t->{exactly} ? '==' : '>=';
+        cmp_ok(scalar @$bugs, $operator, $expected_count,
+               'The right number of bugs are returned');
+        unless ($t->{user} and $t->{user} eq PRIVATE_BUG_USER) {
+            ok(!grep($_->{alias} && $_->{alias} eq $private_bug->{alias}, @$bugs),
+               'Result does not contain the private bug');
+        }
+
+        my @include = @{ $t->{args}->{include_fields} || [] };
+        my @exclude = @{ $t->{args}->{exclude_fields} || [] };
+        if (@include or @exclude) {
+            my @check_fields = uniq (keys %$public_bug, @include);
+            foreach my $field (sort @check_fields) {
+                next if $field eq 'description';
+                if ((@include and !grep { $_ eq $field } @include )
+                    or (@exclude and grep { $_ eq $field } @exclude))
+                {
+                    ok(!exists $bugs->[0]->{$field}, "$field is not included")
+                      or diag Dumper($bugs);
+                }
+                else {
+                    ok(exists $bugs->[0]->{$field}, "$field is included");
+                }
+            }
+        }
+
+    }
+    else {
+        is(scalar @$bugs, 0, 'No bugs returned');
+    }
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => \@tests,
+                       method => 'Bug.search', post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_update.t b/xt/webservice/bug_update.t
new file mode 100644 (file)
index 0000000..dfc2f89
--- /dev/null
@@ -0,0 +1,705 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER STANDARD_BUG_TESTS);
+use Storable qw(dclone);
+use Test::More tests => 937;
+
+use constant NONEXISTENT_BUG => 12_000_000;
+
+###############
+# Subroutines #
+###############
+
+# We have to generate different values for each RPC client, so we
+# have a function to generate the tests for each client.
+sub get_tests {
+    my ($config, $rpc) = @_;
+
+    # update doesn't support logged-out users.
+    my @tests = grep { $_->{user} } @{ STANDARD_BUG_TESTS() };
+
+    my ($public_bug, $second_bug) = $rpc->bz_create_test_bugs();
+    my ($public_id, $second_id) = ($public_bug->{id}, $second_bug->{id});
+
+    # Add aliases to both bugs
+    $public_bug->{alias} = random_string(40);
+    $second_bug->{alias} = random_string(40);
+    my $alias_tests = [
+        { user => 'editbugs',
+          args => { ids => [ $public_id ], alias => $public_bug->{alias} },
+          test => 'Add alias to public bug' },
+        { user => 'editbugs',
+          args => { ids => [ $second_id ], alias => $second_bug->{alias} },
+          test => 'Add alias to second bug' },
+    ];
+    $rpc->bz_run_tests(tests => $alias_tests, method => 'Bug.update');
+
+    my $comment_call = $rpc->bz_call_success(
+        'Bug.comments', { ids => [$public_id, $second_id] });
+    $public_bug->{comment} =
+        $comment_call->result->{bugs}->{$public_id}->{comments}->[0];
+    $second_bug->{comment} =
+        $comment_call->result->{bugs}->{$second_id}->{comments}->[0];
+
+    push(@tests, (
+        { args  => { ids => [$public_id] },
+          error => 'You must log in',
+          test  => 'Logged-out users cannot call update' },
+
+        # FIXME We need a permissions test for canedit, but it's so uncommonly
+        # used that it's not a high priority.
+    ));
+
+    my %valid = valid_values($config, $public_bug, $second_bug);
+    my $valid_value_tests = valid_values_to_tests(\%valid, $public_bug);
+    push(@tests, @$valid_value_tests);
+
+    my %invalid = invalid_values($public_bug, $second_bug);
+    my $invalid_value_tests = invalid_values_to_tests(\%invalid, $public_bug);
+    push(@tests, @$invalid_value_tests);
+
+    return \@tests;
+}
+
+sub valid_values {
+    my ($config, $public_bug, $second_bug) = @_;
+
+    my $admin = $config->{'admin_user_login'};
+    my $second_id = $second_bug->{id};
+    my $comment_id = $public_bug->{comment}->{id};
+    my $bug_uri = $config->{browser_url} . '/'
+                  . $config->{bugzilla_installation} . '/show_bug.cgi?id=';
+
+    my %values = (
+        alias => [
+            { value => random_string(20) },
+        ],
+        assigned_to => [
+            { value => $config->{'unprivileged_user_login'} }
+        ],
+        blocks => [
+            { value => { set => [$second_id] },
+              added => $second_id,
+              test  => 'set to second bug' },
+            { value => { remove => [$second_id] },
+              added => '', removed => $second_id,
+              test  =>  'remove second bug' },
+            { value => { add => [$second_id] },
+              added => $second_id, removed => '',
+              test  => 'add second bug' },
+            { value => { set => [] },
+              added => '', removed => $second_id,
+              test  => 'set to nothing' },
+        ],
+
+        cc => [
+            { value => { add => [$admin] },
+              added => $admin, removed => '',
+              test  => 'add admin' },
+            { value => { remove => [$admin] },
+              added => '', removed => $admin,
+              test  =>  'remove admin' },
+            { value => { remove => [$admin] },
+              test  => "removing user who isn't on the list works",
+              no_changes => 1 },
+        ],
+
+        is_cc_accessible => [
+            { value => 0, test => 'set to 0' },
+            { value => 1, test => 'set to 1' },
+        ],
+
+        comment => [
+            { value => { body => random_string(100) }, test => 'public' },
+            { value => { body => random_string(100), is_private => 1 },
+              user  => PRIVATE_BUG_USER, test => 'private' },
+        ],
+
+        comment_is_private => [
+            { value => { $comment_id => 1 },
+              user  => PRIVATE_BUG_USER, test => 'make description private' },
+            { value => { $comment_id => 0 },
+              user  => PRIVATE_BUG_USER, test => 'make description public' },
+        ],
+
+        component => [
+            { value => 'c2' }
+        ],
+
+        deadline => [
+            { value => '2037-01-01' },
+            { value => '', removed => '2037-01-01', test => 'remove' },
+        ],
+
+        dupe_of => [
+            { value => $second_id },
+        ],
+
+        estimated_time => [
+            { value => '10.0' },
+            { value => '0.0', removed => '10.0', test => 'set to zero' },
+        ],
+
+        groups => [
+            { value => { add => ['Master'] },
+              user => 'admin', added => 'Master', test => 'add Master' },
+            { value => { remove => ['Master'] },
+              user => 'admin', added => '', removed => 'Master',
+              test => 'remove Master' },
+        ],
+
+        keywords => [
+            { value => { add => ['test-keyword-1'] },
+              test => 'add one', added => 'test-keyword-1' },
+            { value => { set => ['test-keyword-1', 'test-keyword-2'] },
+              test  => 'set two', added => 'test-keyword-2' },
+            { value => { remove => ['test-keyword-1'] },
+              removed => 'test-keyword-1', added => '',
+              test  => 'remove one' },
+            { value => { set => [] },
+              removed => 'test-keyword-2', added => '',
+              test  => 'set to empty' },
+            { value => { remove => ['test-keyword-2'] },
+              test  => 'removing removed keyword does nothing',
+              no_changes => 1 },
+        ],
+
+        op_sys => [
+            { value => 'All' },
+        ],
+
+        platform => [
+            { value => 'All' },
+        ],
+
+        priority => [
+            { value => 'Normal' },
+        ],
+
+        product => [
+            { value => 'C2 Forever',
+              extra => {
+                component => 'Helium', version => 'unspecified',
+                target_milestone => '---',
+              },
+              test  => 'move to C2 Forever'
+            },
+            # This also tests that the extra fields transfer over properly
+            # when they have identical names in both products.
+            { value => $public_bug->{product},
+              extra => { component => $public_bug->{component} },
+              test  => 'move back to original product' },
+        ],
+
+        qa_contact => [
+            { value => $admin },
+            { value => '', test => 'set blank', removed => $admin },
+            # Reset to the original so that reset_qa_contact can also be tested.
+            { value => $public_bug->{qa_contact} },
+        ],
+
+        remaining_time => [
+            { value => '1000.50' },
+            { value => 0 },
+        ],
+
+        reset_assigned_to => [
+            { value => 1, field => 'assigned_to',
+              added => $config->{permanent_user} },
+        ],
+
+        reset_qa_contact => [
+            { value => 1, field => 'qa_contact', added => '' },
+        ],
+
+        resolution => [
+            { value => 'FIXED', extra => { status => 'RESOLVED' },
+              test => 'to RESOLVED FIXED' },
+            { value => 'INVALID', test => 'just resolution' },
+        ],
+
+        see_also => [
+            { value => { add => [$bug_uri . $second_id] },
+              added => $bug_uri . $second_id, removed => '',
+              test => 'add local bug URI' },
+            { value => { remove => [$bug_uri . $second_id] },
+              removed => $bug_uri . $second_id, added => '',
+              test => 'remove local bug URI' },
+            { value => { remove => ['http://landfill.bugzilla.org/bugzilla-tip/show_bug.cgi?id=1'] },
+              no_changes => 1,
+              test => 'removing non-existent URI works' },
+            { value => { add => [''] },
+              no_changes => 1,
+              test  => 'adding an empty string to see_also does nothing' },
+            { value => { add => [undef] },
+              no_changes => 1,
+              test  => 'adding a null to see_also does nothing' },
+        ],
+
+        status => [
+            # At this point, due to previous tests, the status is RESOLVED,
+            # so changing to CONFIRMED is our only real option if we want to
+            # test a simple open status.
+            { value => 'CONFIRMED' },
+        ],
+
+        severity => [
+            { value => 'critical' },
+        ],
+
+        summary => [
+            { value => random_string(100) },
+        ],
+
+        target_milestone => [
+            { value => 'AnotherMS2' },
+        ],
+
+        url => [
+            { value => 'http://' . random_string(20) . '/' },
+        ],
+
+        version => [
+            { value => 'Another2' },
+        ],
+
+        whiteboard => [
+            { value => random_string(1000) },
+        ],
+
+        work_time => [
+            # FIXME: work_time really needs to start showing up in the changes
+            # hash.
+            { value => '1.2', no_changes => 1 },
+            { value => '-1.2', test => 'negative value', no_changes => 1 },
+        ],
+    );
+
+    $values{depends_on} = $values{blocks};
+    $values{is_creator_accessible} = $values{is_cc_accessible};
+
+    return %values;
+};
+
+sub valid_values_to_tests {
+    my ($valid_values, $public_bug) = @_;
+
+    my @tests;
+    foreach my $field (sort keys %$valid_values) {
+        my @tests_valid = @{ $valid_values->{$field} };
+        foreach my $item (@tests_valid) {
+            my $desc = $item->{test} || 'valid value';
+            my %args = (
+                ids => [$public_bug->{id}],
+                $field => $item->{value},
+                %{ $item->{extra} || {} },
+            );
+            my %test = ( user => 'editbugs', args => \%args, field => $field,
+                         test => "$field: $desc" );
+            foreach my $item_field (qw(no_changes added removed field user)) {
+                next if !exists $item->{$item_field};
+                $test{$item_field} = $item->{$item_field};
+            }
+            push(@tests, \%test);
+        }
+    }
+
+    return \@tests;
+}
+
+sub invalid_values {
+    my ($public_bug, $second_bug) = @_;
+
+    my $public_id = $public_bug->{id};
+    my $second_id = $second_bug->{id};
+
+    my $comment_id = $public_bug->{comment}->{id};
+    my $second_comment_id = $second_bug->{comment}->{id};
+
+    my %values = (
+        alias => [
+            { value => random_string(41),
+              error => 'aliases cannot be longer than',
+              test  => 'alias cannot be too long' },
+            { value => $second_bug->{alias},
+              error => 'has already taken the alias',
+              test  => 'duplicate alias fails' },
+            { value => 123456,
+              error => 'at least one letter',
+              test  => 'numeric alias fails' },
+            { value => random_string(20), ids => [$public_id, $second_id],
+              error => 'aliases when modifying multiple',
+              test  => 'setting alias on multiple bugs fails' },
+        ],
+
+        assigned_to => [
+            { value => random_string(20),
+              error => 'There is no user named',
+              test  => 'changing assigned_to to invalid user fails' },
+            { value => '',
+              error => 'you must provide an address for the new assignee',
+              test  => 'empty assigned_to fails' },
+            # FIXME Also check strict_isolation at some point in the future,
+            # perhaps.
+        ],
+
+        blocks => [
+            { value => { add => [NONEXISTENT_BUG] },
+              error => 'does not exist',
+              test  => 'Non-existent bug number fails in deps' },
+            { value => { add => [$public_id] },
+              error => 'block itself or depend on itself',
+              test  => "can't add this bug itself in a dep field" },
+            # FIXME Could use strict_isolation checks at some point.
+            # FIXME Could use a dependency_loop_multi test.
+        ],
+
+        cc => [
+            { value => { add => [random_string(20)] },
+              error => 'There is no user named',
+              test  => 'adding invalid user to cc fails' },
+            { value => { remove => [random_string(20)] },
+              error => 'There is no user named',
+              test  => 'removing invalid user from cc fails' },
+        ],
+
+        comment => [
+            { value => { body => random_string(100_000) },
+              error => 'cannot be longer',
+              test  => 'comment too long' },
+            { value => { body => random_string(100), is_private => 1 },
+              error => 'comments or attachments as private',
+              test  => 'normal user cannot add private comments' },
+        ],
+
+        comment_is_private => [
+            { value => { $comment_id => 1 },
+              error => 'comments or attachments as private',
+              test  => 'normal user cannot make a comment private' },
+            { value => { $second_comment_id => 1 },
+              error => 'You tried to modify the privacy of comment',
+              user  => PRIVATE_BUG_USER,
+              test  => 'cannot change privacy on a comment on another bug' },
+        ],
+
+        component => [
+            { value => '',
+              error => 'you must first choose a component',
+              test  => 'empty component fails' },
+            { value => random_string(20),
+              error => 'There is no component named',
+              test  => 'invalid component fails' },
+        ],
+
+        deadline => [
+            { value => random_string(20),
+              error => 'is not a legal date',
+              test  => 'Non-date fails in deadline' },
+            { value => '2037',
+              error => 'is not a legal date',
+              test  => 'year alone fails in deadline' },
+        ],
+
+        dupe_of => [
+            { value => undef,
+              error => 'dup_id was not defined',
+              test  => 'undefined dupe_of fails' },
+            { value => NONEXISTENT_BUG,
+              error => 'does not exist',
+              test  => 'Cannot dup to a nonexistent bug' },
+            { value => $public_id,
+              error => 'as a duplicate of itself',
+              test  => 'Cannot dup bug to itself' },
+        ],
+
+        estimated_time => [
+            { value => -1,
+              error => 'less than the minimum allowable value',
+              test  => 'negative estimated_time fails' },
+            { value => 100_000_000,
+              error => 'more than the maximum allowable value',
+              test  => 'too-large estimated_time fails' },
+            { value => random_string(20),
+              error => 'is not a numeric value',
+              test  => 'non-numeric estimated_time fails' },
+            # We use PRIVATE_BUG_USER because he can modify the bug, but
+            # can't change time-tracking fields.
+            { value => '100', user => PRIVATE_BUG_USER,
+              error => 'only a user with the required permissions',
+              test  => 'non-timetracker can not set estimated_time' },
+        ],
+
+        groups => [
+            { value => { add => ['Master'] },
+              error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+              test  => "adding group we don't have access to but is valid fails" },
+            { value => { add => ['QA-Selenium-TEST'] },
+              error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+              test  => 'adding valid group that is not in this product fails' },
+            { value => { add => [random_string(20)] },
+              error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+              test  => 'adding non-existent group fails' },
+            { value => { remove => [random_string(20)] },
+              error => 'either this group does not exist, or you are not allowed to remove bugs from this group',
+              test => 'removing non-existent group fails' },
+        ],
+
+        keywords => [
+            { value => { add => [random_string(20)] },
+              error => 'See the list of available keywords',
+              test  => 'adding invalid keyword fails' },
+            { value => { remove => [random_string(20)] },
+              error => 'See the list of available keywords',
+              test  => 'removing invalid keyword fails' },
+            { value => { set => [random_string(20)] },
+              error => 'See the list of available keywords',
+              test  => 'setting invalid keyword fails' },
+        ],
+
+        op_sys => [
+            { value => random_string(20),
+              error => 'There is no',
+              test  => 'invalid op_sys fails' },
+            { value => '',
+              error => 'You must select/enter',
+              test => 'blank op_sys fails' },
+        ],
+
+        product => [
+            { value => random_string(60),
+              error => "does not exist or you aren't authorized",
+              test  => 'invalid product fails' },
+            { value => '',
+              error => 'You must select/enter a product',
+              test  => 'moving to blank product fails' },
+            { value => 'TestProduct',
+              error => 'There is no component named',
+              test  => 'moving products without other fields fails' },
+            { value => 'QA-Selenium-TEST',
+              extra => { component => 'QA-Selenium-TEST' },
+              error => "does not exist or you aren't authorized",
+              test  => 'moving to inaccessible product fails' },
+            { value => 'QA Entry Only',
+              error => "does not exist or you aren't authorized",
+              test  => 'moving to product where ENTRY is denied fails' },
+        ],
+
+        qa_contact => [
+            { value => random_string(20),
+              error => 'There is no user named',
+              test  => 'changing qa_contact to invalid user fails' },
+        ],
+
+        remaining_time => [
+            { value => -1,
+              error => 'less than the minimum allowable value',
+              test  => 'negative remaining_time fails' },
+            { value => 100_000_000,
+              error => 'more than the maximum allowable value',
+              test  => 'too-large remaining_time fails' },
+            { value => random_string(20),
+              error => 'is not a numeric value',
+              test  => 'non-numeric remaining_time fails' },
+            # We use PRIVATE_BUG_USER because he can modify the bug, but
+            # can't change time-tracking fields.
+            { value => '100', user => PRIVATE_BUG_USER,
+              error => 'only a user with the required permissions',
+              test  => 'non-timetracker can not set remaining_time' },
+        ],
+
+        # We do all the failing resolution tests on the second bug,
+        # because we want to be sure that we're starting from an open
+        # status.
+        resolution => [
+            { value => random_string(20), ids => [$second_id],
+              extra => { status => 'RESOLVED' },
+              error => 'There is no Resolution named',
+              test  => 'invalid resolution fails' },
+            { value => 'FIXED', ids => [$second_id],
+              error => 'You cannot set a resolution for open bugs',
+              test  => 'setting resolution on open bug fails' },
+            { value => 'DUPLICATE', ids => [$second_id],
+              extra => { status => 'RESOLVED' },
+              error => 'id to mark this bug as a duplicate',
+              test  => 'setting DUPLICATE without dup_id fails' },
+            { value => '', ids => [$second_id],
+              extra => { status => 'RESOLVED' },
+              error => 'A valid resolution is required',
+              test => 'blank resolution fails with closed status' },
+        ],
+
+        see_also => [
+            { value => { add => [random_string(20)] },
+              error => 'is not a valid bug number nor an alias',
+              test  => 'random string fails in see_also' },
+            { value => { add => ['http://landfill.bugzilla.org/'] },
+              error => 'See Also URLs should point to one of',
+              test  => 'no show_bug.cgi in see_also URI' },
+        ],
+
+        status => [
+            { value => random_string(20),
+              error => 'There is no status named',
+              test  => 'invalid status fails' },
+            { value => '',
+              error => 'You must select/enter a status',
+              test => 'blank status fails' },
+            # We use the second bug for this because we can guarantee that
+            # it is open.
+            { value => 'VERIFIED', ids => [$second_id],
+              extra => { resolution => 'FIXED' },
+              error => 'You are not allowed to change the bug status from',
+              test  => 'invalid transition fails' },
+        ],
+
+        summary => [
+            { value => random_string(300),
+              error => 'The text you entered in the Summary field is too long',
+              test  => 'too-long summary fails' },
+            { value => '',
+              error => 'You must enter a summary for this bug',
+              test  => 'blank summary fails' },
+        ],
+
+        work_time => [
+            { value => 100_000_000,
+              error => 'more than the maximum allowable value',
+              test  => 'too-large work_time fails' },
+            { value => random_string(20),
+              error => 'is not a numeric value',
+              test  => 'non-numeric work_time fails' },
+            # We use PRIVATE_BUG_USER because he can modify the bug, but
+            # can't change time-tracking fields.
+            { value => '10', user => PRIVATE_BUG_USER,
+              error => 'only a user with the required permissions',
+              test  => 'non-timetracker can not set work_time' },
+        ],
+    );
+
+    $values{depends_on} = $values{blocks};
+
+    foreach my $field (qw(platform priority severity target_milestone version))
+    {
+        my $tests = dclone($values{op_sys});
+        foreach my $test (@$tests) {
+            $test->{test} =~ s/op_sys/$field/g;
+        }
+        $values{$field} = $tests;
+    }
+
+    return %values;
+}
+
+sub invalid_values_to_tests {
+    my ($invalid_values, $public_bug) = @_;
+
+    my @tests;
+    foreach my $field (sort keys %$invalid_values) {
+        my @tests_invalid = @{ $invalid_values->{$field} };
+        foreach my $item (@tests_invalid) {
+            my %args = (
+                ids => $item->{ids} || [$public_bug->{id}],
+                $field => $item->{value},
+                %{ $item->{extra} || {} },
+            );
+            push(@tests, { user => $item->{user} || 'editbugs',
+                           args => \%args,
+                           error => $item->{error},
+                           test => $item->{test} });
+        }
+    }
+
+    return \@tests;
+}
+
+###############
+# Main Script #
+###############
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+$jsonrpc_get->bz_call_fail('Bug.update',
+    { ids => ['public_bug'] },
+    'must use HTTP POST', 'update fails over GET');
+
+sub post_success {
+    my ($call, $t, $rpc) = @_;
+    return if $t->{no_changes};
+    my $field = $t->{field};
+    return if !$field;
+
+    my @bugs = @{ $call->result->{bugs} };
+    foreach my $bug (@bugs) {
+        if ($field =~ /^comment/) {
+            _check_comment($bug, $field, $t, $rpc);
+        }
+        else {
+            _check_changes($bug, $field, $t);
+        }
+    }
+}
+
+sub _check_changes {
+    my ($bug, $field, $t) = @_;
+
+    my $changes = $bug->{changes}->{$field};
+    ok(defined $changes, "$field was changed")
+      or diag Dumper($bug, $t);
+
+    my $new_value = $t->{added};
+    $new_value = $t->{args}->{$field} if !defined $new_value;
+    _test_value($changes->{added}, $new_value, $field, 'added');
+
+    if (defined $t->{removed}) {
+        _test_value($changes->{removed}, $t->{removed}, $field, 'removed');
+    }
+}
+
+sub _test_value {
+    my ($got, $expected, $field, $type) = @_;
+    if ($field eq 'estimated_time' or $field eq 'remaining_time') {
+        cmp_ok($got, '==', $expected, "$field: $type is correct");
+    }
+    else {
+        is($got, $expected, "$field: $type is correct");
+    }
+}
+
+sub _check_comment {
+    my ($bug, $field, $t, $rpc) = @_;
+    my $bug_id = $bug->{id};
+    my $call = $rpc->bz_call_success('Bug.comments', { ids => [$bug_id] });
+    my $comments = $call->result->{bugs}->{$bug_id}->{comments};
+
+    if ($field eq 'comment_is_private') {
+        my $first_private = $comments->[0]->{is_private};
+        my ($expected) = values %{ $t->{args}->{comment_is_private} };
+        cmp_ok($first_private, '==', $expected,
+               'description privacy is correct');
+    }
+    else {
+        my $last_comment = $comments->[-1];
+        my $expected = $t->{args}->{comment}->{body};
+        is($last_comment->{text}, $expected, 'comment added correctly');
+    }
+
+}
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+    $rpc->bz_run_tests(tests => get_tests($config, $rpc),
+        method => 'Bug.update', post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_update_see_also.t b/xt/webservice/bug_update_see_also.t
new file mode 100644 (file)
index 0000000..79c3b5e
--- /dev/null
@@ -0,0 +1,86 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#################################################
+# Test for xmlrpc call to Bug.update_see_also() #
+#################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER STANDARD_BUG_TESTS);
+use Test::More tests => 117;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+my $bug_url = 'http://landfill.bugzilla.org/bugzilla-tip/show_bug.cgi?id=100';
+
+# update_see_also doesn't support logged-out users.
+my @tests = grep { $_->{user} } @{ STANDARD_BUG_TESTS() };
+foreach my $t (@tests) {
+    $t->{args}->{add} = $t->{args}->{remove} = [];
+}
+
+push(@tests, (
+    { user  => 'unprivileged',
+      args  => { ids => ['public_bug'], add => [$bug_url] },
+      error => 'only the assignee or reporter of the bug, or a user',
+      test  => 'Unprivileged user cannot add a URL to a bug',
+    },
+
+    { user  => 'admin',
+      args  => { ids => ['public_bug'], add => ['asdfasdfasdf'] },
+      error => 'asdf',
+      test  => 'Admin cannot add an invalid URL',
+    },
+    { user => 'admin',
+      args => { ids => ['public_bug'], remove => ['asdfasdfasdf'] },
+      test => 'Invalid URL silently ignored',
+    },
+
+    { user => 'admin',
+      args => { ids => ['public_bug'], add => [$bug_url] },
+      test => 'Admin can add a URL to a public bug',
+    },
+    { user  => 'unprivileged',
+      args  => { ids => ['public_bug'], remove => [$bug_url] },
+      error => 'only the assignee or reporter of the bug, or a user',
+      test  => 'Unprivileged user cannot remove a URL from a bug',
+    },
+    { user => 'admin',
+      args => { ids => ['public_bug'], remove => [$bug_url] },
+      test => 'Admin can remove a URL from a public bug',
+    },
+
+    { user => PRIVATE_BUG_USER,
+      args => { ids => ['private_bug'], add => [$bug_url] },
+      test => PRIVATE_BUG_USER . ' can add a URL to a private bug',
+    },
+    { user => PRIVATE_BUG_USER,
+      args => { ids => ['private_bug'], remove => [$bug_url] },
+      test => PRIVATE_BUG_USER . ' can remove a URL from a private bug',
+    },
+
+));
+
+sub post_success {
+    my ($call, $t) = @_;
+    isa_ok($call->result->{changes}, 'HASH', "Changes");
+}
+
+$jsonrpc_get->bz_call_fail('Bug.update_see_also',
+    { ids => ['public_bug'], add => [$bug_url] },
+    'must use HTTP POST', 'update_see_also fails over GET');
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+    $rpc->bz_run_tests(tests => \@tests, method => 'Bug.update_see_also',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/bugzilla.t b/xt/webservice/bugzilla.t
new file mode 100644 (file)
index 0000000..2ddb130
--- /dev/null
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##################################################
+# Test for xmlrpc call functions in Bugzilla.pm  #
+##################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 11 * 3;
+use QA::Util;
+my ($config, @clients) = get_rpc_clients();
+
+foreach my $rpc (@clients) {
+    my $vers_call = $rpc->bz_call_success('Bugzilla.version');
+    my $version = $vers_call->result->{version};
+    ok($version, "Bugzilla.version returns $version");
+
+    my $tz_call = $rpc->bz_call_success('Bugzilla.timezone');
+    my $tz = $tz_call->result->{timezone};
+    ok($tz, "Bugzilla.timezone retuns $tz");
+
+    my $ext_call = $rpc->bz_call_success('Bugzilla.extensions');
+    my $extensions = $ext_call->result->{extensions};
+    isa_ok($extensions, 'HASH', 'extensions');
+
+    # There is always at least the QA extension enabled.
+    my $cmp = $config->{test_extensions} ? '>' : '==';
+    my @ext_names = keys %$extensions;
+    my $desc = scalar(@ext_names) . ' extension(s) returned: ' . join(', ', @ext_names);
+    cmp_ok(scalar(@ext_names), $cmp, 1, $desc);
+    ok(grep($_ eq 'QA', @ext_names), 'The QA extension is enabled');
+
+    my $time_call = $rpc->bz_call_success('Bugzilla.time');
+    my $time_result = $time_call->result;
+    foreach my $type (qw(db_time web_time)) {
+        cmp_ok($time_result->{$type}, '=~', $rpc->DATETIME_REGEX,
+               "Bugzilla.time returns a datetime for $type");
+    }
+}
diff --git a/xt/webservice/group_create.t b/xt/webservice/group_create.t
new file mode 100644 (file)
index 0000000..e46546a
--- /dev/null
@@ -0,0 +1,101 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##########################################
+# Test for xmlrpc call to Group.create() #
+##########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 77;
+use QA::Util;
+
+use constant DESCRIPTION => 'Group created by Group.create';
+
+sub post_success {
+    my $call = shift;
+    my $gid = $call->result->{id};
+    ok($gid, "Got a non-zero group ID: $gid");
+}
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+my @tests = (
+    { args  => { name => random_string(20), description => DESCRIPTION },
+      error => 'You must log in',
+      test  => 'Logged-out user cannot call Group.create',
+    },
+    { user  => 'unprivileged',
+      args  => { name => random_string(20), description => DESCRIPTION },
+      error => 'you are not authorized',
+      test  => 'Unprivileged user cannot call Group.create',
+    },
+    { user  => 'admin',
+      args  => { description => DESCRIPTION },
+      error => 'You must enter a name',
+      test  => 'Missing name to Group.create',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20) },
+      error => 'You must enter a description',
+      test  => 'Missing description to Group.create',
+    },
+    { user  => 'admin',
+      args  => { name => '', description => DESCRIPTION },
+      error => 'You must enter a name',
+      test  => 'Name to Group.create cannot be empty',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20), description => '' },
+      error => 'You must enter a description',
+      test  => 'Description to Group.create cannot be empty',
+    },
+    { user  => 'admin',
+      args  => { name => 'canconfirm', description => DESCRIPTION },
+      error => 'already exists',
+      test  => 'Name to Group.create already exists',
+    },
+    { user  => 'admin',
+      args  => { name => 'caNConFIrm', description => DESCRIPTION },
+      error => 'already exists',
+      test  => 'Name to Group.create already exists but with a different case',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20), description => DESCRIPTION,
+                 user_regexp => '\\'},
+      error => 'The regular expression you entered is invalid',
+      test  => 'The regular expression passed to Group.create is invalid',
+    },
+);
+
+$jsonrpc_get->bz_call_fail('Group.create',
+    { name => random_string(20), description => 'Created with JSON-RPC via GET' },
+    'must use HTTP POST', 'Group.create fails over GET');
+
+foreach my $rpc ($xmlrpc, $jsonrpc) {
+    # Tests which work must be called from here,
+    # to avoid creating twice the same group.
+    my @all_tests = (@tests,
+        { user  => 'admin',
+          args  => { name => random_string(20), description => DESCRIPTION },
+          test  => 'Passing the name and description only works',
+        },
+        { user  => 'admin',
+          args  => { name => random_string(20), description => DESCRIPTION,
+                     user_regexp => '\@foo.com$', is_active => 1,
+                     icon_url => 'http://www.bugzilla.org/favicon.ico' },
+          test  => 'Passing all arguments works',
+        },
+    );
+    $rpc->bz_run_tests(tests => \@all_tests, method => 'Group.create',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/jsonp.t b/xt/webservice/jsonp.t
new file mode 100644 (file)
index 0000000..75a0c0c
--- /dev/null
@@ -0,0 +1,34 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 85;
+use QA::Util;
+my $jsonrpc_get = QA::Util::get_jsonrpc_client('GET');
+
+my @chars = (0..9, 'A'..'Z', 'a'..'z', '_[].');
+
+our @tests = (
+    { args => { callback => join('', @chars) },
+      test => 'callback accepts all legal characters.' },
+);
+foreach my $char (qw(! ~ ` @ $ % ^ & * - + = { } ; : ' " < > / ? |),
+                  '(', ')', '\\', '#', ',')
+{
+    push(@tests,
+         { args  => { callback => "a$char" },
+           error => "as your 'callback' parameter",
+           test  => "$char is not valid in callback" });
+}
+
+$jsonrpc_get->bz_run_tests(method => 'Bugzilla.version', tests => \@tests);
diff --git a/xt/webservice/product_create.t b/xt/webservice/product_create.t
new file mode 100644 (file)
index 0000000..0ca117c
--- /dev/null
@@ -0,0 +1,167 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+############################################
+# Test for xmlrpc call to Product.create() #
+############################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 121;
+use QA::Util;
+
+use constant DESCRIPTION => 'Product created by Product.create';
+use constant PROD_VERSION => 'unspecified';
+
+sub post_success {
+    my ($call, $test, $self) = @_;
+    my $args = $test->{args};
+    my $prod_id = $call->result->{id};
+    ok($prod_id, "Got a non-zero product ID: $prod_id");
+
+    $call = $self->bz_call_success("Product.get", {ids => [$prod_id]});
+    my $product = $call->result->{products}->[0];
+    my $prod_name = $product->{name};
+    my $is_active = defined $args->{is_open} ? $args->{is_open} : 1;
+    ok($product->{is_active} == $is_active,
+       "Product $prod_name has the correct value for is_active/is_open: $is_active");
+    my $has_unco = defined $args->{has_unconfirmed} ? $args->{has_unconfirmed} : 1;
+    ok($product->{has_unconfirmed} == $has_unco,
+       "Product $prod_name has the correct value for has_unconfirmed: $has_unco");
+}
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+my @tests = (
+    { args  => { name => random_string(20), version => PROD_VERSION,
+                 description => DESCRIPTION },
+      error => 'You must log in',
+      test  => 'Logged-out user cannot call Product.create',
+    },
+    { user  => 'unprivileged',
+      args  => { name => random_string(20), version => PROD_VERSION,
+                 description => DESCRIPTION },
+      error => 'you are not authorized',
+      test  => 'Unprivileged user cannot call Product.create',
+    },
+    { user  => 'admin',
+      args  => { version => PROD_VERSION, description => DESCRIPTION },
+      error => 'You must enter a name',
+      test  => 'Missing name to Product.create',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20), version => PROD_VERSION },
+      error => 'You must enter a description',
+      test  => 'Missing description to Product.create',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20), description => DESCRIPTION },
+      error => 'You must enter a valid version',
+      test  => 'Missing version to Product.create',
+    },
+    { user  => 'admin',
+      args  => { name => '', version => PROD_VERSION, description => DESCRIPTION },
+      error => 'You must enter a name',
+      test  => 'Name to Product.create cannot be empty',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20), version => PROD_VERSION, description => '' },
+      error => 'You must enter a description',
+      test  => 'Description to Product.create cannot be empty',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20), version => '', description => DESCRIPTION },
+      error => 'You must enter a valid version',
+      test  => 'Version to Product.create cannot be empty',
+    },
+    { user  => 'admin',
+      args  => { name => random_string(20000), version => PROD_VERSION,
+                 description => DESCRIPTION },
+      error => 'The name of a product is limited',
+      test  => 'Name to Product.create too long',
+    },
+    { user  => 'admin',
+      args  => { name => 'Another Product', version => PROD_VERSION,
+                 description => DESCRIPTION },
+      error => 'already exists',
+      test  => 'Name to Product.create already exists',
+    },
+    { user  => 'admin',
+      args  => { name => 'aNoThEr Product', version => PROD_VERSION,
+                 description => DESCRIPTION },
+      error => 'differs from existing product',
+      test  => 'Name to Product.create already exists but with a different case',
+    },
+);
+
+# FIXME - Should be: if (classifications enabled).
+# But there is currently now way to query the value of a parameter via WS.
+if (0) {
+    push(@tests,
+        { user  => 'admin',
+          args  => { name => random_string(20), version => PROD_VERSION,
+                     description => DESCRIPTION, has_unconfirmed => 1,
+                     classification => '', default_milestone => '2.0',
+                     is_open => 1, create_series => 1 },
+          error => 'You must select/enter a classification',
+          test  => 'Passing an empty classification to Product.create fails',
+        },
+        { user  => 'admin',
+          args  => { name => random_string(20), version => PROD_VERSION,
+                     description => DESCRIPTION, has_unconfirmed => 1,
+                     classification => random_string(10), default_milestone => '2.0',
+                     is_open => 1, create_series => 1 },
+          error => 'You must select/enter a classification',
+          test  => 'Passing an invalid classification to Product.create fails',
+        },
+    )
+}
+
+$jsonrpc_get->bz_call_fail('Product.create',
+    { name => random_string(20), version => PROD_VERSION,
+      description => 'Created with JSON-RPC via GET' },
+    'must use HTTP POST', 'Product.create fails over GET');
+
+foreach my $rpc ($xmlrpc, $jsonrpc) {
+    # Tests which work must be called from here,
+    # to avoid creating twice the same product.
+    my @all_tests = (@tests,
+        { user  => 'admin',
+          args  => { name => random_string(20), version => PROD_VERSION,
+                     description => DESCRIPTION },
+          test  => 'Passing the name, description and version only works',
+        },
+        { user  => 'admin',
+          args  => { name => random_string(20), version => PROD_VERSION,
+                     description => DESCRIPTION, has_unconfirmed => 1,
+                     classification => 'Class2_QA', default_milestone => '2.0',
+                     is_open => 1, create_series => 1 },
+          test  => 'Passing all arguments works',
+        },
+        { user  => 'admin',
+          args  => { name => random_string(20), version => PROD_VERSION,
+                     description => DESCRIPTION, has_unconfirmed => 0,
+                     classification => 'Class2_QA', default_milestone => '2.0',
+                     is_open => 0, create_series => 0 },
+          test  => 'Passing null values works',
+        },
+        { user  => 'admin',
+          args  => { name => random_string(20), version => PROD_VERSION,
+                     description => DESCRIPTION, has_unconfirmed => 1,
+                     classification => 'Class2_QA', default_milestone => '',
+                     is_open => 1, create_series => 1 },
+          test  => 'Passing an empty default milestone works (falls back to "---")',
+        },
+    );
+    $rpc->bz_run_tests(tests => \@all_tests, method => 'Product.create',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/product_get.t b/xt/webservice/product_get.t
new file mode 100644 (file)
index 0000000..5cc6022
--- /dev/null
@@ -0,0 +1,113 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+########################################
+# Test for xmlrpc calls to:            #
+# Product.get_selectable_products()    #
+# Product.get_enterable_products()     #
+# Product.get_accessible_products()    #
+# Product.get()                        #
+########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 134;
+use QA::Util;
+my ($config, @clients) = get_rpc_clients();
+
+my $products = $clients[0]->bz_get_products();
+my $public    = $products->{'Another Product'};
+my $private   = $products->{'QA-Selenium-TEST'};
+my $no_entry  = $products->{'QA Entry Only'};
+my $no_search = $products->{'QA Search Only'};
+
+my %id_map = reverse %$products;
+
+my $tests = {
+    'QA_Selenium_TEST' => {
+        selectable => [$public, $private, $no_entry, $no_search],
+        enterable  => [$public, $private, $no_entry, $no_search],
+        accessible => [$public, $private, $no_entry, $no_search],
+    },
+    'unprivileged' => {
+        selectable => [$public, $no_entry],
+        not_selectable => $no_search,
+        enterable  => [$public, $no_search],
+        not_enterable => $no_entry,
+        accessible => [$public, $no_entry, $no_search],
+        not_accessible => $private,
+    },
+    '' => {
+        selectable => [$public, $no_entry],
+        not_selectable => $no_search,
+        enterable  => [$public, $no_search],
+        not_enterable => $no_entry,
+        accessible => [$public, $no_entry, $no_search],
+        not_accessible => $private,
+    },
+};
+
+foreach my $rpc (@clients) {
+    foreach my $user (keys %$tests) {
+        my @selectable = @{ $tests->{$user}->{selectable} };
+        my @enterable  = @{ $tests->{$user}->{enterable} };
+        my @accessible = @{ $tests->{$user}->{accessible} };
+        my $not_selectable = $tests->{$user}->{not_selectable};
+        my $not_enterable  = $tests->{$user}->{not_enterable};
+        my $not_accessible = $tests->{$user}->{not_accessible};
+
+        $rpc->bz_log_in($user) if $user;
+        $user ||= "Logged-out user";
+
+        my $select_call =
+            $rpc->bz_call_success('Product.get_selectable_products');
+        my $select_ids = $select_call->result->{ids};
+        foreach my $id (@selectable) {
+            ok(grep($_ == $id, @$select_ids),
+               "$user can select " . $id_map{$id});
+        }
+        if ($not_selectable) {
+            ok(!grep($_ == $not_selectable, @$select_ids),
+               "$user cannot select " . $id_map{$not_selectable});
+        }
+
+        my $enter_call =
+            $rpc->bz_call_success('Product.get_enterable_products');
+        my $enter_ids = $enter_call->result->{ids};
+        foreach my $id (@enterable) {
+            ok(grep($_ == $id, @$enter_ids), "$user can enter " . $id_map{$id});
+        }
+        if ($not_enterable) {
+            ok(!grep($_ == $not_enterable, @$enter_ids),
+               "$user cannot enter " . $id_map{$not_enterable});
+        }
+
+        my $access_call =
+            $rpc->bz_call_success('Product.get_accessible_products');
+        my $get_call = $rpc->bz_call_success('Product.get',
+                                             { ids => \@accessible });
+        my $products = $get_call->result->{products};
+        my $expected_count = scalar @accessible;
+        cmp_ok(scalar @$products, '==', $expected_count,
+           "Product.get gets all $expected_count accessible products"
+           . " for $user.");
+        if ($not_accessible) {
+            my $no_access_call = $rpc->bz_call_success(
+                'Product.get', { ids => [$not_accessible] });
+            ok(!scalar @{ $no_access_call->result->{products} },
+               "$user gets 0 products when asking for "
+               . $id_map{$not_accessible});
+        }
+
+        $rpc->bz_call_success('User.logout') if $user ne "Logged-out user";
+    }
+}
diff --git a/xt/webservice/user_create.t b/xt/webservice/user_create.t
new file mode 100644 (file)
index 0000000..38b55e6
--- /dev/null
@@ -0,0 +1,118 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#########################################
+# Test for xmlrpc call to User.Create() #
+#########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use Test::More tests => 75;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+use constant NEW_PASSWORD => 'password';
+use constant NEW_FULLNAME => 'WebService Created User';
+
+use constant PASSWORD_TOO_SHORT => 'a';
+
+# These are the characters that are actually invalid per RFC.
+use constant INVALID_EMAIL => '()[]\;:,<>@webservice.test';
+
+sub new_login {
+    return 'created_' . random_string(@_) . '@webservice.test';
+}
+
+sub post_success {
+    my ($call) = @_;
+    ok($call->result->{id}, "Got a non-zero user id");
+}
+
+$jsonrpc_get->bz_call_fail('User.create',
+    { email => new_login(), full_name => NEW_FULLNAME,
+      password => '*' },
+    'must use HTTP POST', 'User.create fails over GET');
+
+# We have to wrap @tests in the foreach, because we want a different
+# login for each user, separately for each RPC client. (You can't create
+# two users with the same username, and XML-RPC would otherwise try to
+# create the same users that JSON-RPC created.)
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+    my @tests = (
+        # Permissions checks
+        { args  => { email    => new_login(), full_name => NEW_FULLNAME,
+                     password => NEW_PASSWORD },
+          error => "you are not authorized",
+          test  => 'Logged-out user cannot call User.create',
+        },
+        { user  => 'unprivileged',
+          args  => { email    => new_login(), full_name => NEW_FULLNAME,
+                     password => NEW_PASSWORD },
+          error => "you are not authorized",
+          test  => 'Unprivileged user cannot call User.create',
+        },
+
+        # Login name checks.
+        { user  => 'admin',
+          args  => { full_name => NEW_FULLNAME, password => NEW_PASSWORD },
+          error => "argument was not set",
+          test  => 'Leaving out email argument fails',
+        },
+        { user  => 'admin',
+          args  => { email    => '', full_name => NEW_FULLNAME,
+                     password => NEW_PASSWORD },
+          error => "argument was not set",
+          test  => "Passing an empty email argument fails",
+        },
+        { user  => 'admin',
+          args  => { email    => INVALID_EMAIL, full_name => NEW_FULLNAME,
+                     password => NEW_PASSWORD },
+          error =>  "didn't pass our syntax checking",
+          test  => 'Invalid email address fails',
+        },
+        { user  => 'admin',
+          args  => { email    => new_login(128), full_name => NEW_FULLNAME,
+            password => NEW_PASSWORD },
+            error =>  "didn't pass our syntax checking",
+            test  => 'Too long (> 127 chars) email address fails',
+        },
+        { user  => 'admin',
+          args  => { email     => $config->{unprivileged_user_login},
+                     full_name => NEW_FULLNAME, password => NEW_PASSWORD },
+          error =>  "There is already an account",
+          test  => 'Trying to use an existing login name fails',
+        },
+
+        { user  => 'admin',
+          args  => { email    => new_login(), full_name => NEW_FULLNAME,
+                     password => PASSWORD_TOO_SHORT },
+          error => 'password must be at least',
+          test  => 'Password Too Short fails',
+        },
+        { user => 'admin',
+          args => { email    => new_login(), full_name => NEW_FULLNAME,
+                    password => NEW_PASSWORD },
+          test => 'Creating a user with all arguments and correct privileges',
+        },
+        { user => 'admin',
+          args => { email => new_login(), password => NEW_PASSWORD },
+          test => 'Leaving out fullname works',
+        },
+        { user => 'admin',
+          args => { email => new_login(), full_name => NEW_FULLNAME },
+          test => 'Leaving out password works',
+        },
+    );
+
+    $rpc->bz_run_tests(tests => \@tests, method => 'User.create',
+                       post_success => \&post_success);
+}
diff --git a/xt/webservice/user_get.t b/xt/webservice/user_get.t
new file mode 100644 (file)
index 0000000..02cf00f
--- /dev/null
@@ -0,0 +1,222 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+######################################
+# Test for xmlrpc call to User.get() #
+######################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER);
+use Test::More tests => 330;
+our ($config, @clients) = get_rpc_clients();
+
+my $get_user = $config->{'unprivileged_user_login'};
+my $canconfirm_user = $config->{'canconfirm_user_login'};
+my $priv_user = $config->{PRIVATE_BUG_USER . '_user_login'};
+my $disabled = $config->{'disabled_user_login'};
+my $disabled_match = substr($disabled, 0, length($disabled) - 1);
+
+# These are the basic tests. There are tests for include_fields
+# and exclude_field below.
+
+my @tests = (
+    { args => { names => [$get_user] },
+      test => "Logged-out user can get unprivileged user by name"
+    },
+    { args  => { match => [$get_user] },
+      test  => 'Logged-out user cannot use the match argument',
+      error => 'Logged-out users cannot use',
+    },
+    { args  => { ids => [1] },
+      test  => 'Logged-out users cannot use the "ids" argument',
+      error => 'Logged-out users cannot use',
+    },
+
+    # match & names
+    { user => 'unprivileged',
+      args => { names => [$get_user] },
+      test => "Unprivileged user can get himself",
+    },
+    { user => 'unprivileged',
+      args => { match => [$get_user] },
+      test => 'Logged-in user can use the match argument',
+    },
+    { user => 'unprivileged',
+      args => { match => [$get_user], names => [$get_user] },
+      test => 'Specifying the same thing in "match" and "names"',
+    },
+
+    # include_disabled
+    { user => 'unprivileged',
+      args => { match => [$get_user, $disabled_match] },
+      test => 'Disabled users are not normally returned'
+    },
+    { user => 'unprivileged',
+      args => { match => [$disabled_match], include_disabled => 1 },
+      test => 'Specifying include_disabled returns disabled users'
+    },
+    { user => 'unprivileged',
+      args => { match => [$disabled] },
+      test => 'Full match on a disabled user returns that user',
+    },
+
+    # groups and group_ids
+    { args  => { groups => ['QA-Selenium-TEST'] },
+      test  => 'Specifying just groups fails',
+      error => 'one of the following parameters',
+    },
+    { args => { group_ids => [1] },
+      test => 'Specifying just group ids fails',
+      error => 'one of the following parameters',
+    },
+    { args => { names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST'] },
+      test => 'Limiting the return value to a group while being logged out fails',
+      error => 'The group you specified, QA-Selenium-TEST, is not valid here',
+    },
+    { user => 'unprivileged',
+      args => { names => [$get_user, $priv_user], groups => ['missing_group'] },
+      test => 'Limiting the return value to a group which does not exist fails',
+      error => 'The group you specified, missing_group, is not valid here',
+    },
+    { user => 'unprivileged',
+      args => { names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST'] },
+      test => 'Limiting the return value to a group you do not belong to fails',
+      error => 'The group you specified, QA-Selenium-TEST, is not valid here',
+    },
+    { user => 'editbugs',
+      args => { names => [$get_user, $priv_user], groups => ['canconfirm', 'editbugs'] },
+      test => 'Limiting the return value to some groups you do not belong to fails',
+      error => 'The group you specified, canconfirm, is not valid here',
+    },
+    { user => 'admin',
+      args => { names => [$canconfirm_user], groups => ['canconfirm', 'editbugs'] },
+      test => 'Limiting the return value to groups you belong to',
+    },
+
+    # groups returned
+    { user => 'admin',
+      args => { names => [$get_user] },
+      test => 'Admin can get user',
+    },
+    { user => 'admin',
+      args => { names => [$canconfirm_user] },
+      test => 'Admin can get user',
+    },
+    { user => 'canconfirm',
+      args => { names => [$canconfirm_user] },
+      test => 'Privileged user can get himself',
+    },
+    { user => 'editbugs',
+      args => { names => [$canconfirm_user] },
+      test => 'Privileged user can get another user',
+    },
+);
+
+sub post_success {
+    my ($call, $t) = @_;
+
+    my $result = $call->result;
+    is(scalar @{ $result->{users} }, 1, "Got exactly one user");
+    my $item = $result->{users}->[0];
+    my $user = $t->{user} || '';
+
+    if ($user eq 'admin') {
+        ok(exists $item->{email} && exists $item->{can_login}
+           && exists $item->{email_enabled} && exists $item->{login_denied_text},
+           'Admin correctly gets all user fields');
+    }
+    elsif ($user) {
+        ok(exists $item->{email} && exists $item->{can_login},
+           'Logged-in user correctly gets email and can_login');
+        ok(!exists $item->{email_enabled}
+           && !exists $item->{login_denied_text},
+           "Non-admin user doesn't get email_enabled and login_denied_text");
+    }
+    else {
+        my @item_keys = sort keys %$item;
+        is_deeply(\@item_keys, ['id', 'name', 'real_name'],
+            'Only id, name, and real_name are returned to logged-out users');
+        return;
+    }
+
+    my $username = $config->{"${user}_user_login"};
+    # FIXME We have no way to create a saved search or a saved report from
+    # the WebService, so we cannot test that the correct data is returned
+    # if the user is accessing his own account.
+    if ($username eq $item->{name}) {
+        ok(exists $item->{saved_searches} && exists $item->{saved_reports},
+           'Users can get the list of saved searches and reports for their own account');
+    }
+    else {
+        ok(!exists $item->{saved_searches} && !exists $item->{saved_reports},
+           "Users cannot get the list of saved searches and reports from someone else's acccount");
+    }
+
+    my @groups = map { $_->{name} } @{$item->{groups}};
+    # Admins can see all groups a user belongs to (assuming they inherited
+    # membership for all groups). Same for a user querying his own account.
+    if ($username eq $item->{name} || $user eq 'admin') {
+        if ($username eq $get_user) {
+            ok(!scalar @groups, "The unprivileged user doesn't belong to any group");
+        }
+        elsif ($username eq $canconfirm_user) {
+            ok(grep($_ eq 'canconfirm', @groups), "Group 'canconfirm' returned");
+        }
+    }
+    else {
+        ok(!scalar @groups, "No groups are visible to users without bless privs");
+    }
+}
+
+foreach my $rpc (@clients) {
+    $rpc->bz_run_tests(tests => \@tests, method => 'User.get',
+                       post_success => \&post_success);
+
+    #############################
+    # Include and Exclude Tests #
+    #############################
+
+    my $include_nothing = $rpc->bz_call_success('User.get', {
+        names => [$get_user], include_fields => ['asdfasdfsdf'],
+    }, 'User.get including only invalid fields');
+    is(scalar keys %{ $include_nothing->result->{users}->[0] }, 0,
+       'No fields returned for user');
+
+    my $include_one = $rpc->bz_call_success('User.get', {
+        names => [$get_user], include_fields => ['id'],
+    }, 'User.get including only id');
+    is(scalar keys %{ $include_one->result->{users}->[0] }, 1,
+       'Only one field returned for user');
+
+    my $exclude_none = $rpc->bz_call_success('User.get', {
+        names => [$get_user], exclude_fields => ['asdfasdfsdf'],
+    }, 'User.get excluding only invalid fields');
+    is(scalar keys %{ $exclude_none->result->{users}->[0] }, 3,
+       'All fields returned for user');
+
+    my $exclude_one = $rpc->bz_call_success('User.get', {
+        names => [$get_user], exclude_fields => ['id'],
+    }, 'User.get excluding id');
+    is(scalar keys %{ $exclude_one->result->{users}->[0] }, 2,
+       'Only two fields returned for user');
+
+    my $override = $rpc->bz_call_success('User.get', {
+        names => [$get_user], include_fields => ['id', 'name'],
+        exclude_fields => ['id']
+    }, 'User.get with both include and exclude');
+    is(scalar keys %{ $override->result->{users}->[0] }, 1,
+       'Only one field returned');
+    ok(exists $override->result->{users}->[0]->{name},
+       '...and that field is the "name" field');
+}
diff --git a/xt/webservice/user_login_logout.t b/xt/webservice/user_login_logout.t
new file mode 100644 (file)
index 0000000..fd5f8ef
--- /dev/null
@@ -0,0 +1,128 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##########################################################
+# Test for xmlrpc call to User.login() and User.logout() #
+##########################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use QA::Util;
+use Test::More tests => 119;
+my ($config, @clients) = get_rpc_clients();
+
+use constant INVALID_EMAIL => '@invalid_user@';
+
+my $user = $config->{unprivileged_user_login};
+my $pass = $config->{unprivileged_user_passwd};
+my $error = "The login or password you entered is not valid";
+
+my @tests = (
+    { user => 'unprivileged',
+      test => "Unprivileged user can log in successfully",
+    },
+
+    { args  => { login => $user, password => '' },
+      error => $error,
+      test  => "Empty password can't log in",
+    },
+    { args  => { login => '', password => $pass },
+      error => $error,
+      test  => "Empty login can't log in",
+    },
+    { args  => { login => $user },
+      error => "requires a password argument",
+      test  => "Undef password can't log in",
+    },
+    { args  => { password => $pass },
+      error => "requires a login argument",
+      test  => "Undef login can't log in",
+    },
+
+    { args  => { login => INVALID_EMAIL, password => $pass },
+      error => $error,
+      test  => "Invalid email can't log in",
+    },
+    { args  => { login => $user, password => '*' },
+      error => $error,
+      test  => "Invalid password can't log in",
+    },
+
+    { args  => { login    => $config->{disabled_user_login},
+                 password => $config->{disabled_user_passwd} },
+      error => "!!This is the text!!",
+      test  => "Can't log in with a disabled account",
+    },
+    { args  => { login => $config->{disabled_user_login}, password => '*' },
+      error => $error,
+      test  => "Logging in with invalid password doesn't show disabledtext",
+    },
+);
+
+sub _login_args {
+    my $args = shift;
+    my %fixed_args = %$args;
+    $fixed_args{Bugzilla_login} = delete $fixed_args{login};
+    $fixed_args{Bugzilla_password} = delete $fixed_args{password};
+    return \%fixed_args;
+}
+
+foreach my $rpc (@clients) {
+    if ($rpc->bz_get_mode) {
+        $rpc->bz_call_fail('User.logout', undef, 'must use HTTP POST',
+                           'User.logout fails when called via GET');
+    }
+
+    foreach my $t (@tests) {
+        if ($t->{user}) {
+            my $username = $config->{$t->{user} . '_user_login'};
+            my $password = $config->{$t->{user} . '_user_passwd'};
+
+            if ($rpc->bz_get_mode) {
+                $rpc->bz_call_fail('User.login',
+                    { login => $username, password => $password },
+                    'must use HTTP POST', $t->{test} . ' (fails on GET)');
+            }
+            else {
+                $rpc->bz_log_in($t->{user});
+                ok($rpc->{_bz_credentials}->{token}, 'Login token returned');
+                $rpc->bz_call_success('User.logout');
+            }
+
+            if ($t->{error}) {
+                $rpc->bz_call_fail('Bugzilla.version',
+                    { Bugzilla_login => $username,
+                      Bugzilla_password => $password });
+            }
+            else {
+                $rpc->bz_call_success('Bugzilla.version',
+                    { Bugzilla_login => $username,
+                      Bugzilla_password => $password });
+            }
+        }
+        else {
+            # Under GET, there's no reason to have extra failing tests.
+            if (!$rpc->bz_get_mode) {
+                $rpc->bz_call_fail('User.login', $t->{args}, $t->{error},
+                                   $t->{test});
+            }
+            if (defined $t->{args}->{login}
+                and defined $t->{args}->{password})
+            {
+                my $fixed_args = _login_args($t->{args});
+                $rpc->bz_call_fail('Bugzilla.version', $fixed_args,
+                    $t->{error}, "Bugzilla_login: " . $t->{test});
+            }
+        }
+    }
+}
diff --git a/xt/webservice/user_offer_account_by_email.t b/xt/webservice/user_offer_account_by_email.t
new file mode 100644 (file)
index 0000000..7859321
--- /dev/null
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#########################################################
+# Test for xmlrpc call to User.offer_account_by_email() #
+#########################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use Test::More tests => 29;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+# These are the characters that are actually invalid per RFC.
+use constant INVALID_EMAIL => '()[]\;:,<>@webservice.test';
+
+sub new_login {
+    return 'requested_' . random_string() . '@webservice.test';
+}
+
+$jsonrpc_get->bz_call_fail('User.offer_account_by_email',
+    { email => new_login() },
+    'must use HTTP POST', 'offer_account_by_email fails over GET');
+
+# Have to wrap @tests in the foreach so that new_login returns something
+# different each time.
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+    my @tests = (
+        # Login name checks.
+        { args  => { },
+          error => "argument was not set",
+          test  => 'Leaving out email argument fails',
+        },
+        { args  => { email => '' },
+          error => "argument was not set",
+          test  => "Passing an empty email argument fails",
+        },
+        { args  => { email => INVALID_EMAIL },
+          error => "didn't pass our syntax checking",
+          test  => 'Invalid email address fails',
+        },
+        { args  => { email => $config->{unprivileged_user_login} },
+          error => "There is already an account",
+          test  => 'Trying to use an existing login name fails',
+        },
+
+        { args => { email => new_login() },
+          test => 'Valid, non-existing email passes.',
+        },
+    );
+
+    $rpc->bz_run_tests(tests => \@tests,
+                       method => 'User.offer_account_by_email');
+}