summaryrefslogtreecommitdiffstats
path: root/xt
diff options
context:
space:
mode:
Diffstat (limited to 'xt')
-rw-r--r--xt/README18
-rw-r--r--xt/config/checksetup_answers.txt25
-rwxr-xr-xxt/config/generate_test_data.pl616
-rw-r--r--xt/config/patch.diff18
-rw-r--r--xt/config/selenium_test.conf46
-rw-r--r--xt/extensions/QA/Config.pm22
-rw-r--r--xt/extensions/QA/Extension.pm74
-rw-r--r--xt/extensions/QA/lib/Util.pm28
-rw-r--r--xt/extensions/QA/template/en/default/hook/README5
-rw-r--r--xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl7
-rw-r--r--xt/extensions/QA/template/en/default/qa/README16
-rw-r--r--xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl17
-rw-r--r--xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl33
-rw-r--r--xt/extensions/QA/template/en/default/qa/results.html.tmpl28
-rw-r--r--xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl13
-rw-r--r--xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl29
-rw-r--r--xt/extensions/QA/web/README7
-rw-r--r--xt/lib/Bugzilla/Test/Search.pm987
-rw-r--r--xt/lib/Bugzilla/Test/Search/AndTest.pm52
-rw-r--r--xt/lib/Bugzilla/Test/Search/Constants.pm1203
-rw-r--r--xt/lib/Bugzilla/Test/Search/CustomTest.pm101
-rw-r--r--xt/lib/Bugzilla/Test/Search/FieldTest.pm617
-rw-r--r--xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm104
-rw-r--r--xt/lib/Bugzilla/Test/Search/InjectionTest.pm77
-rw-r--r--xt/lib/Bugzilla/Test/Search/NotTest.pm61
-rw-r--r--xt/lib/Bugzilla/Test/Search/OperatorTest.pm103
-rw-r--r--xt/lib/Bugzilla/Test/Search/OrTest.pm141
-rw-r--r--xt/lib/QA/REST.pm65
-rw-r--r--xt/lib/QA/RPC.pm289
-rw-r--r--xt/lib/QA/RPC/JSONRPC.pm174
-rw-r--r--xt/lib/QA/RPC/XMLRPC.pm26
-rw-r--r--xt/lib/QA/Tests.pm115
-rw-r--r--xt/lib/QA/Util.pm372
-rw-r--r--xt/rest/bugzilla.t60
-rw-r--r--xt/rest/classification.t61
-rw-r--r--xt/search.t82
-rw-r--r--xt/selenium/bug_edit.t441
-rw-r--r--xt/selenium/choose_priority.t30
-rw-r--r--xt/selenium/classifications.t142
-rw-r--r--xt/selenium/config.t48
-rw-r--r--xt/selenium/create_user_accounts.t139
-rw-r--r--xt/selenium/custom_fields.t462
-rw-r--r--xt/selenium/custom_fields_admin.t56
-rw-r--r--xt/selenium/dependencies.t56
-rw-r--r--xt/selenium/edit_products_properties.t338
-rw-r--r--xt/selenium/email_preferences.t405
-rw-r--r--xt/selenium/enter_new_bug.t35
-rw-r--r--xt/selenium/flags.t441
-rw-r--r--xt/selenium/flags2.t308
-rw-r--r--xt/selenium/groups.t378
-rw-r--r--xt/selenium/keywords.t181
-rw-r--r--xt/selenium/login.t37
-rw-r--r--xt/selenium/milestones.t149
-rw-r--r--xt/selenium/password_complexity.t123
-rw-r--r--xt/selenium/private_attachments.t173
-rw-r--r--xt/selenium/qa_contact.t164
-rw-r--r--xt/selenium/require_login.t83
-rw-r--r--xt/selenium/sanity_check.t49
-rw-r--r--xt/selenium/saved_searches.t117
-rw-r--r--xt/selenium/search.t71
-rw-r--r--xt/selenium/security.t198
-rw-r--r--xt/selenium/shared_searches.t199
-rw-r--r--xt/selenium/show_all_products.t56
-rw-r--r--xt/selenium/shutdown.t77
-rw-r--r--xt/selenium/status_whiteboard.t118
-rw-r--r--xt/selenium/strict_isolation.t145
-rw-r--r--xt/selenium/sudo_sessions.t158
-rw-r--r--xt/selenium/target_milestones.t111
-rw-r--r--xt/selenium/time_summary.t101
-rw-r--r--xt/selenium/user_groups.t249
-rw-r--r--xt/selenium/user_matching.t188
-rw-r--r--xt/selenium/user_preferences.t225
-rw-r--r--xt/selenium/user_privs.t60
-rw-r--r--xt/selenium/votes.t233
-rw-r--r--xt/webservice/bug_add_attachment.t231
-rw-r--r--xt/webservice/bug_add_comment.t173
-rw-r--r--xt/webservice/bug_attachments.t155
-rw-r--r--xt/webservice/bug_comments.t178
-rw-r--r--xt/webservice/bug_create.t243
-rw-r--r--xt/webservice/bug_fields.t223
-rw-r--r--xt/webservice/bug_get.t150
-rw-r--r--xt/webservice/bug_history.t33
-rw-r--r--xt/webservice/bug_legal_values.t104
-rw-r--r--xt/webservice/bug_search.t211
-rw-r--r--xt/webservice/bug_update.t705
-rw-r--r--xt/webservice/bug_update_see_also.t86
-rw-r--r--xt/webservice/bugzilla.t49
-rw-r--r--xt/webservice/group_create.t101
-rw-r--r--xt/webservice/jsonp.t34
-rw-r--r--xt/webservice/product_create.t167
-rw-r--r--xt/webservice/product_get.t113
-rw-r--r--xt/webservice/user_create.t118
-rw-r--r--xt/webservice/user_get.t222
-rw-r--r--xt/webservice/user_login_logout.t128
-rw-r--r--xt/webservice/user_offer_account_by_email.t63
95 files changed, 12177 insertions, 3546 deletions
diff --git a/xt/README b/xt/README
deleted file mode 100644
index 22f9f171b..000000000
--- 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
index 000000000..54473d3ed
--- /dev/null
+++ b/xt/config/checksetup_answers.txt
@@ -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
index 000000000..ab186698d
--- /dev/null
+++ b/xt/config/generate_test_data.pl
@@ -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
index 000000000..cbaff0249
--- /dev/null
+++ b/xt/config/patch.diff
@@ -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
index 000000000..e4d7e4417
--- /dev/null
+++ b/xt/config/selenium_test.conf
@@ -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
index 000000000..b4f6bc9a2
--- /dev/null
+++ b/xt/extensions/QA/Config.pm
@@ -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
index 000000000..5befe3e36
--- /dev/null
+++ b/xt/extensions/QA/Extension.pm
@@ -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
index 000000000..e299adcc9
--- /dev/null
+++ b/xt/extensions/QA/lib/Util.pm
@@ -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
index 000000000..3f1e487e2
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/hook/README
@@ -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
index 000000000..bcb75107d
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl
@@ -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
index 000000000..604d00cfe
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/qa/README
@@ -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
index 000000000..5a83a6c5b
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl
@@ -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
index 000000000..997378343
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl
@@ -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
index 000000000..a2f812697
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/qa/results.html.tmpl
@@ -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
index 000000000..f37c00262
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl
@@ -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
index 000000000..fd093d3b0
--- /dev/null
+++ b/xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl
@@ -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
index 000000000..23456410f
--- /dev/null
+++ b/xt/extensions/QA/web/README
@@ -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
index ca3bba5cf..000000000
--- a/xt/lib/Bugzilla/Test/Search.pm
+++ /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
index f34ba1f3a..000000000
--- a/xt/lib/Bugzilla/Test/Search/AndTest.pm
+++ /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
index 5d84ec6ff..000000000
--- a/xt/lib/Bugzilla/Test/Search/Constants.pm
+++ /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
index 132e5ac40..000000000
--- a/xt/lib/Bugzilla/Test/Search/CustomTest.pm
+++ /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
index 5e86d92e2..000000000
--- a/xt/lib/Bugzilla/Test/Search/FieldTest.pm
+++ /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
index 888e7eb13..000000000
--- a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm
+++ /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
index 90eaabc78..000000000
--- a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm
+++ /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
index 190b8567b..000000000
--- a/xt/lib/Bugzilla/Test/Search/NotTest.pm
+++ /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
index 5ab502dfc..000000000
--- a/xt/lib/Bugzilla/Test/Search/OperatorTest.pm
+++ /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
index 1b948f38d..000000000
--- a/xt/lib/Bugzilla/Test/Search/OrTest.pm
+++ /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
index 000000000..4de985668
--- /dev/null
+++ b/xt/lib/QA/REST.pm
@@ -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
index 000000000..63a7d9503
--- /dev/null
+++ b/xt/lib/QA/RPC.pm
@@ -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
index 000000000..7a085e026
--- /dev/null
+++ b/xt/lib/QA/RPC/JSONRPC.pm
@@ -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
index 000000000..cb227fa9c
--- /dev/null
+++ b/xt/lib/QA/RPC/XMLRPC.pm
@@ -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
index 000000000..fe5f2d067
--- /dev/null
+++ b/xt/lib/QA/Tests.pm
@@ -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
index 000000000..e122e41db
--- /dev/null
+++ b/xt/lib/QA/Util.pm
@@ -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
index 000000000..a176d1cf1
--- /dev/null
+++ b/xt/rest/bugzilla.t
@@ -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
index 000000000..d006de984
--- /dev/null
+++ b/xt/rest/classification.t
@@ -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
index 8f6e2e998..000000000
--- a/xt/search.t
+++ /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
index 000000000..46e7e6cb3
--- /dev/null
+++ b/xt/selenium/bug_edit.t
@@ -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
index 000000000..1089d2003
--- /dev/null
+++ b/xt/selenium/choose_priority.t
@@ -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
index 000000000..4d5d012f0
--- /dev/null
+++ b/xt/selenium/classifications.t
@@ -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
index 000000000..b99927321
--- /dev/null
+++ b/xt/selenium/config.t
@@ -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
index 000000000..7c71273a6
--- /dev/null
+++ b/xt/selenium/create_user_accounts.t
@@ -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
index 000000000..6c0c8fa5d
--- /dev/null
+++ b/xt/selenium/custom_fields.t
@@ -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
index 000000000..d0ffb9db8
--- /dev/null
+++ b/xt/selenium/custom_fields_admin.t
@@ -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
index 000000000..133e17e07
--- /dev/null
+++ b/xt/selenium/dependencies.t
@@ -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
index 000000000..1f9851729
--- /dev/null
+++ b/xt/selenium/edit_products_properties.t
@@ -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
index 000000000..c1e60b05a
--- /dev/null
+++ b/xt/selenium/email_preferences.t
@@ -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
index 000000000..404d30f10
--- /dev/null
+++ b/xt/selenium/enter_new_bug.t
@@ -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
index 000000000..dd4a0ffa8
--- /dev/null
+++ b/xt/selenium/flags.t
@@ -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
index 000000000..9b921612c
--- /dev/null
+++ b/xt/selenium/flags2.t
@@ -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
index 000000000..b755cafc8
--- /dev/null
+++ b/xt/selenium/groups.t
@@ -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
index 000000000..16ecf90e7
--- /dev/null
+++ b/xt/selenium/keywords.t
@@ -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
index 000000000..b41d9a2d2
--- /dev/null
+++ b/xt/selenium/login.t
@@ -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
index 000000000..35991fbd2
--- /dev/null
+++ b/xt/selenium/milestones.t
@@ -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
index 000000000..e29ef8bec
--- /dev/null
+++ b/xt/selenium/password_complexity.t
@@ -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
index 000000000..4dacd26b3
--- /dev/null
+++ b/xt/selenium/private_attachments.t
@@ -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
index 000000000..c548a7182
--- /dev/null
+++ b/xt/selenium/qa_contact.t
@@ -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
index 000000000..d661121b5
--- /dev/null
+++ b/xt/selenium/require_login.t
@@ -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
index 000000000..93b039d41
--- /dev/null
+++ b/xt/selenium/sanity_check.t
@@ -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
index 000000000..a18b7fd49
--- /dev/null
+++ b/xt/selenium/saved_searches.t
@@ -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
index 000000000..c8fa9d770
--- /dev/null
+++ b/xt/selenium/search.t
@@ -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
index 000000000..b89ea114e
--- /dev/null
+++ b/xt/selenium/security.t
@@ -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
index 000000000..f9443fa98
--- /dev/null
+++ b/xt/selenium/shared_searches.t
@@ -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
index 000000000..894554665
--- /dev/null
+++ b/xt/selenium/show_all_products.t
@@ -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
index 000000000..8751cd614
--- /dev/null
+++ b/xt/selenium/shutdown.t
@@ -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
index 000000000..3ddda7b86
--- /dev/null
+++ b/xt/selenium/status_whiteboard.t
@@ -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
index 000000000..ecd72b207
--- /dev/null
+++ b/xt/selenium/strict_isolation.t
@@ -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
index 000000000..5a1b7c98b
--- /dev/null
+++ b/xt/selenium/sudo_sessions.t
@@ -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
index 000000000..6c5cf637f
--- /dev/null
+++ b/xt/selenium/target_milestones.t
@@ -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
index 000000000..f60c952ca
--- /dev/null
+++ b/xt/selenium/time_summary.t
@@ -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
index 000000000..aa393d47b
--- /dev/null
+++ b/xt/selenium/user_groups.t
@@ -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
index 000000000..90c2dc608
--- /dev/null
+++ b/xt/selenium/user_matching.t
@@ -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
index 000000000..0d7d87a5c
--- /dev/null
+++ b/xt/selenium/user_preferences.t
@@ -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
index 000000000..f48792839
--- /dev/null
+++ b/xt/selenium/user_privs.t
@@ -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
index 000000000..e5a7c853d
--- /dev/null
+++ b/xt/selenium/votes.t
@@ -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
index 000000000..f08e42c6c
--- /dev/null
+++ b/xt/webservice/bug_add_attachment.t
@@ -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
index 000000000..6f234b37a
--- /dev/null
+++ b/xt/webservice/bug_add_comment.t
@@ -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
index 000000000..d5283685d
--- /dev/null
+++ b/xt/webservice/bug_attachments.t
@@ -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
index 000000000..d66e445cf
--- /dev/null
+++ b/xt/webservice/bug_comments.t
@@ -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
index 000000000..6d7c8e14a
--- /dev/null
+++ b/xt/webservice/bug_create.t
@@ -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
index 000000000..097a607f5
--- /dev/null
+++ b/xt/webservice/bug_fields.t
@@ -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
index 000000000..e05fe2cb2
--- /dev/null
+++ b/xt/webservice/bug_get.t
@@ -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
index 000000000..02ec1c11a
--- /dev/null
+++ b/xt/webservice/bug_history.t
@@ -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
index 000000000..2f775e528
--- /dev/null
+++ b/xt/webservice/bug_legal_values.t
@@ -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
index 000000000..93a517e24
--- /dev/null
+++ b/xt/webservice/bug_search.t
@@ -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
index 000000000..dfc2f89e1
--- /dev/null
+++ b/xt/webservice/bug_update.t
@@ -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
index 000000000..79c3b5ea8
--- /dev/null
+++ b/xt/webservice/bug_update_see_also.t
@@ -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
index 000000000..2ddb13092
--- /dev/null
+++ b/xt/webservice/bugzilla.t
@@ -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
index 000000000..e46546a31
--- /dev/null
+++ b/xt/webservice/group_create.t
@@ -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
index 000000000..75a0c0cfb
--- /dev/null
+++ b/xt/webservice/jsonp.t
@@ -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
index 000000000..0ca117c31
--- /dev/null
+++ b/xt/webservice/product_create.t
@@ -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
index 000000000..5cc6022d5
--- /dev/null
+++ b/xt/webservice/product_get.t
@@ -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
index 000000000..38b55e69a
--- /dev/null
+++ b/xt/webservice/user_create.t
@@ -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
index 000000000..02cf00fe7
--- /dev/null
+++ b/xt/webservice/user_get.t
@@ -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
index 000000000..fd5f8ef6b
--- /dev/null
+++ b/xt/webservice/user_login_logout.t
@@ -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
index 000000000..785932167
--- /dev/null
+++ b/xt/webservice/user_offer_account_by_email.t
@@ -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');
+}