summaryrefslogtreecommitdiffstats
path: root/qa/t
diff options
context:
space:
mode:
authorDavid Lawrence <dkl@mozilla.com>2015-06-25 05:35:16 +0200
committerDavid Lawrence <dkl@mozilla.com>2015-06-25 05:35:16 +0200
commita6238e079b4e8e62d3e23f019e0895c626133c83 (patch)
tree5c840202b0e02237a9a6b6acfdd613d78d38624a /qa/t
parent28f425dac57db417b2a701fa99ef4ad73da0a729 (diff)
downloadbugzilla-a6238e079b4e8e62d3e23f019e0895c626133c83.tar.gz
bugzilla-a6238e079b4e8e62d3e23f019e0895c626133c83.tar.xz
Bug 1144485: Adapt upstream Selenium test suite to BMO
Diffstat (limited to 'qa/t')
-rw-r--r--qa/t/lib/QA/RPC.pm284
-rw-r--r--qa/t/lib/QA/RPC/JSONRPC.pm168
-rw-r--r--qa/t/lib/QA/RPC/XMLRPC.pm19
-rw-r--r--qa/t/lib/QA/Tests.pm108
-rw-r--r--qa/t/lib/QA/Util.pm375
-rw-r--r--qa/t/selenium_server_start.t83
-rw-r--r--qa/t/selenium_server_stop.t34
-rw-r--r--qa/t/test_bmo_autolinkification.t53
-rw-r--r--qa/t/test_bmo_enter_new_bug.t421
-rw-r--r--qa/t/test_bmo_retire_values.t388
-rw-r--r--qa/t/test_bug_edit.t504
-rw-r--r--qa/t/test_choose_priority.t27
-rw-r--r--qa/t/test_classifications.t146
-rw-r--r--qa/t/test_config.t45
-rw-r--r--qa/t/test_create_user_accounts.t129
-rw-r--r--qa/t/test_custom_fields.t459
-rw-r--r--qa/t/test_custom_fields_admin.t53
-rw-r--r--qa/t/test_default_groups.t210
-rw-r--r--qa/t/test_dependencies.t53
-rw-r--r--qa/t/test_edit_products_properties.t361
-rw-r--r--qa/t/test_email_preferences.t400
-rw-r--r--qa/t/test_enter_new_bug.t35
-rw-r--r--qa/t/test_flags.t482
-rw-r--r--qa/t/test_flags2.t344
-rw-r--r--qa/t/test_groups.t323
-rw-r--r--qa/t/test_keywords.t176
-rw-r--r--qa/t/test_login.t32
-rw-r--r--qa/t/test_long_list_redirection.t20
-rw-r--r--qa/t/test_milestones.t170
-rw-r--r--qa/t/test_private_attachments.t200
-rw-r--r--qa/t/test_qa_contact.t167
-rw-r--r--qa/t/test_require_login.t81
-rw-r--r--qa/t/test_sanity_check.t46
-rw-r--r--qa/t/test_saved_searches.t114
-rw-r--r--qa/t/test_search.t71
-rw-r--r--qa/t/test_security.t177
-rw-r--r--qa/t/test_shared_searches.t196
-rw-r--r--qa/t/test_show_all_products.t57
-rw-r--r--qa/t/test_shutdown.t75
-rw-r--r--qa/t/test_status_whiteboard.t98
-rw-r--r--qa/t/test_strict_isolation.t155
-rw-r--r--qa/t/test_sudo_sessions.t143
-rw-r--r--qa/t/test_target_milestones.t109
-rw-r--r--qa/t/test_time_summary.t100
-rw-r--r--qa/t/test_user_groups.t229
-rw-r--r--qa/t/test_user_matching.t188
-rw-r--r--qa/t/test_user_preferences.t237
-rw-r--r--qa/t/test_user_privs.t68
-rw-r--r--qa/t/test_votes.t251
-rw-r--r--qa/t/webservice_bug_add_attachment.t227
-rw-r--r--qa/t/webservice_bug_add_comment.t168
-rw-r--r--qa/t/webservice_bug_attachments.t151
-rw-r--r--qa/t/webservice_bug_comments.t175
-rw-r--r--qa/t/webservice_bug_create.t239
-rw-r--r--qa/t/webservice_bug_fields.t219
-rw-r--r--qa/t/webservice_bug_get.t146
-rw-r--r--qa/t/webservice_bug_get_bugs.t146
-rw-r--r--qa/t/webservice_bug_history.t29
-rw-r--r--qa/t/webservice_bug_legal_values.t100
-rw-r--r--qa/t/webservice_bug_search.t185
-rw-r--r--qa/t/webservice_bug_update.t683
-rw-r--r--qa/t/webservice_bug_update_see_also.t82
-rw-r--r--qa/t/webservice_bugzilla.t45
-rw-r--r--qa/t/webservice_group_create.t97
-rw-r--r--qa/t/webservice_jsonp.t30
-rw-r--r--qa/t/webservice_product.t109
-rw-r--r--qa/t/webservice_product_create.t163
-rw-r--r--qa/t/webservice_product_get.t109
-rw-r--r--qa/t/webservice_user_create.t108
-rw-r--r--qa/t/webservice_user_get.t218
-rw-r--r--qa/t/webservice_user_login_logout.t124
-rw-r--r--qa/t/webservice_user_offer_account_by_email.t59
72 files changed, 12276 insertions, 0 deletions
diff --git a/qa/t/lib/QA/RPC.pm b/qa/t/lib/QA/RPC.pm
new file mode 100644
index 000000000..4053c4dfe
--- /dev/null
+++ b/qa/t/lib/QA/RPC.pm
@@ -0,0 +1,284 @@
+# 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 strict;
+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->{alias} = random_string(40);
+ $public_bug->{whiteboard} = join(' ', @whiteboard_strings);
+ $public_bug->{summary} = join(' ', @summary_strings);
+
+ my $private_bug = dclone($public_bug);
+ $private_bug->{alias} = random_string(40);
+ 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/qa/t/lib/QA/RPC/JSONRPC.pm b/qa/t/lib/QA/RPC/JSONRPC.pm
new file mode 100644
index 000000000..4175b10fc
--- /dev/null
+++ b/qa/t/lib/QA/RPC/JSONRPC.pm
@@ -0,0 +1,168 @@
+# 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 strict;
+
+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/qa/t/lib/QA/RPC/XMLRPC.pm b/qa/t/lib/QA/RPC/XMLRPC.pm
new file mode 100644
index 000000000..d88d4092e
--- /dev/null
+++ b/qa/t/lib/QA/RPC/XMLRPC.pm
@@ -0,0 +1,19 @@
+# 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 strict;
+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/qa/t/lib/QA/Tests.pm b/qa/t/lib/QA/Tests.pm
new file mode 100644
index 000000000..0bd0942fa
--- /dev/null
+++ b/qa/t/lib/QA/Tests.pm
@@ -0,0 +1,108 @@
+# 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 strict;
+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/qa/t/lib/QA/Util.pm b/qa/t/lib/QA/Util.pm
new file mode 100644
index 000000000..d8d8ae52f
--- /dev/null
+++ b/qa/t/lib/QA/Util.pm
@@ -0,0 +1,375 @@
+# 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 strict;
+use Data::Dumper;
+use Test::More;
+use Test::WWW::Selenium;
+use WWW::Selenium::Util qw(server_is_running);
+
+# Fixes wide character warnings
+BEGIN {
+ my $builder = Test::More->builder;
+ binmode $builder->output, ":encoding(utf8)";
+ binmode $builder->failure_output, ":encoding(utf8)";
+ binmode $builder->todo_output, ":encoding(utf8)";
+}
+
+use base 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
+
+ WAIT_TIME
+ CHROME_MODE
+);
+
+# How long we wait for pages to load.
+use constant WAIT_TIME => 60000;
+use constant CONF_FILE => "../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': $!$@";
+}
+
+sub get_selenium {
+ my $chrome_mode = shift;
+ my $config = get_config();
+
+ if (!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 = new QA::RPC::XMLRPC(proxy => $xmlrpc_url);
+ return ($rpc, $config);
+}
+
+sub get_jsonrpc_client {
+ my ($get_mode) = @_;
+ require QA::RPC::JSONRPC;
+ my $rpc = new QA::RPC::JSONRPC();
+ # 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 ($sel->is_text_present("Which product is affected by the problem")) {
+ ok(1, "Which product is affected by the problem");
+ $sel->click_ok("link=Other Products", undef, "Choose full product list");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $title = $sel->get_title();
+ }
+ if ($sel->is_text_present($product)) {
+ ok(1, "Display the list of enterable products");
+ $sel->open_ok("/bmo/enter_bug.cgi?product=$product&format=__default__", undef, "Choose product $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");
+ # Always make sure all fields are visible
+ if ($sel->is_element_present('//input[@value="Show Advanced Fields"]')) {
+ $sel->click_ok('//input[@value="Show Advanced Fields"]');
+ }
+}
+
+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 $btn_id = $options ? $options->{id} : 'commit';
+ $sel->click_ok($btn_id);
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->is_text_present_ok("Changes submitted for 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/qa/t/selenium_server_start.t b/qa/t/selenium_server_start.t
new file mode 100644
index 000000000..c08db293c
--- /dev/null
+++ b/qa/t/selenium_server_start.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 strict;
+use warnings;
+
+use constant DISPLAY => 99;
+#use constant DISPLAY => 0;
+
+use Test::More tests => 12;
+#use Test::More tests => 4;
+
+my $pid;
+
+# Start the Xvfb server first
+$pid = xserver_start();
+ok($pid, "X Server started with PID $pid on display " . DISPLAY);
+ok(open(XPID, ">testing.x.pid"), "Opening testing.x.pid");
+ok((print XPID $pid), "Writing testing.x.pid");
+ok(close(XPID), "Closing testing.x.pid");
+
+# Start the VNC service second
+ok($pid = vnc_start(), "VNC desktop started with PID $pid");
+ok(open(VNCPID, ">testing.vnc.pid"), "Opening testing.vnc.pid");
+ok((print VNCPID $pid), "Writing testing.vnc.pid");
+ok(close(VNCPID), "Closing testing.vnc.pid");
+
+# Start the selenium server third
+ok($pid = selenium_start(), "Selenium RC server started with PID $pid");
+ok(open(SPID, ">testing.selenium.pid"), "Opening testing.selenium.pid");
+ok((print SPID $pid), "Writing testing.selenium.pid");
+ok(close(SPID), "Closing testing.selenium.pid");
+
+sleep(10);
+
+# Subroutines
+
+sub xserver_start {
+ my $pid;
+ my @x_cmd = qw(Xvfb -ac -screen 0 1600x1200x24 -fbdir /tmp);
+ push(@x_cmd, ":" . DISPLAY);
+ $pid = fork();
+ if (!$pid) {
+ open(STDOUT, ">/dev/null");
+ open(STDERR, ">/dev/null");
+ exec(@x_cmd) || die "unable to execute: $!";
+ }
+ else {
+ return $pid;
+ }
+ return 0;
+}
+
+sub vnc_start {
+ my @vnc_cmd = qw(x11vnc -viewonly -forever -nopw -quiet -display);
+ push(@vnc_cmd, ":" . DISPLAY);
+ my $pid = fork();
+ if (!$pid) {
+ open(STDOUT, ">/dev/null");
+ open(STDERR, ">/dev/null");
+ exec(@vnc_cmd) || die "unabled to execute: $!";
+ }
+ return $pid;
+}
+
+sub selenium_start {
+ my @selenium_cmd = qw(java -jar ../config/selenium-server-standalone.jar
+ -firefoxProfileTemplate ../config/firefox
+ -log ../config/selenium.log
+ -singlewindow);
+ unshift(@selenium_cmd, "env", "DISPLAY=:" . DISPLAY);
+ my $pid = fork();
+ if (!$pid) {
+ open(STDOUT, ">/dev/null");
+ open(STDERR, ">/dev/null");
+ exec(@selenium_cmd) || die "unable to execute: $!";
+ }
+ return $pid;
+}
diff --git a/qa/t/selenium_server_stop.t b/qa/t/selenium_server_stop.t
new file mode 100644
index 000000000..62a29f38c
--- /dev/null
+++ b/qa/t/selenium_server_stop.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 strict;
+use warnings;
+
+use Test::More tests => 15;
+
+my $pid;
+
+# Stop the selenium server first
+ok(open(SPID, "<testing.selenium.pid"), "Opening testing.selenium.pid");
+ok(($pid = <SPID>), "Reading testing.selenium.pid");
+ok(close(SPID), "Closing testing.selenium.pid");
+ok(kill(9, $pid), "Killing process $pid");
+ok(unlink("testing.selenium.pid"), "Removing testing.selenium.pid");
+
+# Stop the VNC service second
+ok(open(VNCPID, "<testing.vnc.pid"), "Opening testing.vnc.pid");
+ok(($pid = <VNCPID>), "Reading testing.vnc.pid");
+ok(close(VNCPID), "Closing testing.vnc.pid");
+ok(kill(9, $pid), "Killing process $pid");
+ok(unlink("testing.vnc.pid"), "Removing testing.vnc.pid");
+
+# Stop the Xvfb server third
+ok(open(XPID, "<testing.x.pid"), "Opening testing.x.pid");
+ok(($pid = <XPID>), "Reading testing.x.pid");
+ok(close(XPID), "Closing testing.x.pid");
+ok(kill(9, $pid), "Killing process $pid");
+ok(unlink("testing.x.pid"), "Removing testing.x.pid");
diff --git a/qa/t/test_bmo_autolinkification.t b/qa/t/test_bmo_autolinkification.t
new file mode 100644
index 000000000..520301165
--- /dev/null
+++ b/qa/t/test_bmo_autolinkification.t
@@ -0,0 +1,53 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+log_in($sel, $config, 'unprivileged');
+file_bug_in_product($sel, 'TestProduct');
+my $bug_summary = "linkification test bug";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "linkification test");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/\d+ \S $bug_summary/, "Bug created");
+my $bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+
+$sel->type_ok("comment", "bp-63f096f7-253b-4ee2-ae3d-8bb782090824");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/\d+ \S $bug_summary/, "crash report added");
+$sel->click_ok("link=bug $bug_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->attribute_is('link=bp-63f096f7-253b-4ee2-ae3d-8bb782090824@href', 'https://crash-stats.mozilla.com/report/index/63f096f7-253b-4ee2-ae3d-8bb782090824');
+
+$sel->type_ok("comment", "CVE-2010-2884");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/\d+ \S $bug_summary/, "cve added");
+$sel->click_ok("link=bug $bug_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->attribute_is('link=CVE-2010-2884@href', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2884');
+
+$sel->type_ok("comment", "r12345");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/\d+ \S $bug_summary/, "svn revision added");
+$sel->click_ok("link=bug $bug_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->attribute_is('link=r12345@href', 'https://viewvc.svn.mozilla.org/vc?view=rev&revision=12345');
+
+logout($sel);
+
diff --git a/qa/t/test_bmo_enter_new_bug.t b/qa/t/test_bmo_enter_new_bug.t
new file mode 100644
index 000000000..ac4a9ca8b
--- /dev/null
+++ b/qa/t/test_bmo_enter_new_bug.t
@@ -0,0 +1,421 @@
+# 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.
+
+# Comments:
+# 1. Some of the forms have been commented as they have been removed since
+# this script was originally created. I left them in insteading of deleting
+# so they could be used for reference for adding new form tests.
+# 2. The _check_* utility functions for creating objects should be moved to
+# generate_test_data.pl at some point.
+
+use strict;
+use warnings;
+use lib qw(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-off" => undef} });
+
+# mktgevent and swag are dependent so we create the mktgevent bug first so
+# we can provide the bug id to swag
+
+## mktgevent
+#
+#_check_product('Marketing');
+#_check_component('Marketing', 'Event Requests');
+#_check_component('Marketing', 'Swag Requests');
+#_check_group('mozilla-corporation-confidential');
+#
+## FIXME figure out how to use format= with file_bug_in_product
+#
+#$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Marketing&format=mktgevent");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->title_is("Event Request Form", "Open custom bug entry form - mktgevent");
+#$sel->type_ok("firstname", "Bugzilla", "Enter first name");
+#$sel->type_ok("lastname", "Administrator", "Enter last name");
+#$sel->type_ok("email", $config->{'admin_user_login'}, "Enter email address");
+#$sel->type_ok("eventname", "Event Name", "Enter event name");
+#$sel->type_ok("website", $config->{'browser_url'}, "Enter web site");
+#$sel->type_ok("goals", "Goals for the event", "Enter goals");
+#$sel->type_ok("date", "2032/01/01", "Enter date");
+#$sel->type_ok("successmeasure", "Success Measure", "Enter measure of success");
+#$sel->click_ok("doing", "value=Other", "Select what doing");
+#$sel->type_ok("doing-other-what", "What will you be doing at the event", "Enter other what doing");
+#$sel->select_ok("attendees", "value=1-99", "Select number of attendees");
+#$sel->select_ok("audience", "value=Contributors", "Select targeted audience");
+#$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->is_text_present_ok('has been added to the database', 'Bug created');
+#my $mktgevent_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+#
+## swag
+#
+#$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Marketing&format=swag");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->title_is("Swag Request Form", "Open custom bug entry form - swag");
+#$sel->type_ok("firstname", "Bugzilla", "Enter first name");
+#$sel->type_ok("lastname", "Administrator", "Enter last name");
+#$sel->type_ok("dependson", $mktgevent_bug_id, "Enter event request bug id");
+#$sel->type_ok("email", $config->{'admin_user_login'}, "Enter email address");
+#$sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address");
+#$sel->type_ok("additional", "Specific swag needed", "Enter specific swag needed");
+#$sel->type_ok("shiptofirstname", "Bugzilla", "Enter ship to first name");
+#$sel->type_ok("shiptolastname", "Administrator", "Enter ship to last name");
+#$sel->type_ok("shiptoaddress", "100 Some Street", "Enter ship to address");
+#$sel->type_ok("shiptoaddress2", "Suite 200", "Enter ship to address 2");
+#$sel->type_ok("shiptocity", "Mountain View", "Enter ship to city");
+#$sel->type_ok("shiptostate", "California", "Enter ship to state");
+#$sel->type_ok("shiptocountry", "USA", "Enter ship to country");
+#$sel->type_ok("shiptopcode", "94041", "Enter ship to postal code");
+#$sel->type_ok("shiptophone", "1-800-555-1212", "Enter ship to phone");
+#$sel->type_ok("shiptoidrut", "What is this?", "Enter ship to personal id/rut");
+#$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description");
+#$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->is_text_present_ok('has been added to the database', 'Bug created');
+#my $swag_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# trademark
+
+_check_product('Marketing');
+_check_component('Marketing', 'Trademark Permissions');
+_check_group('marketing-private');
+
+$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Marketing&format=trademark");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Trademark Usage Requests", "Open custom bug entry form - trademark");
+$sel->type_ok("short_desc", "Bug created by Selenium", "Enter bug summary");
+$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description");
+$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+my $trademark_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# itrequest
+
+_check_product('mozilla.org');
+_check_product('Infrastructure & Operations');
+_check_component('Infrastructure & Operations', 'WebOps: Other');
+_check_version('Infrastructure & Operations', 'other');
+_check_group('infra');
+
+#$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=mozilla.org&format=itrequest");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->title_is("Mozilla Corporation/Foundation IT Requests", "Open custom bug entry form - itrequest");
+#$sel->click_ok("component_webops_other", "Select request type");
+#$sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address");
+#$sel->type_ok("short_desc", "Bug created by Selenium", "Enter request summary");
+#$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter request description");
+#$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->is_text_present_ok('has been added to the database', 'Bug created');
+#my $itrequest_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# brownbag
+
+#$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=mozilla.org&format=brownbag");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->title_is("Mozilla Corporation Brownbag Requests", "Open custom bug entry form - brownbag");
+#$sel->type_ok("presenter", "Bugzilla Administrator", "Enter presenter");
+#$sel->type_ok("topic", "Automated testing of Bugzilla", "Enter topic");
+#$sel->type_ok("date", "01/01/2012", "Enter date");
+#$sel->select_ok("time_hour", "value=1", "Select hour");
+#$sel->select_ok("time_minute", "value=30", "Select minute");
+#$sel->select_ok("ampm", "value=PM", "Select am/pm");
+#$sel->select_ok("audience", "value=Employees Only", "Select audience");
+#$sel->check_ok("airmozilla", "Select need airmozilla");
+#$sel->check_ok("dialin", "Select need dial in");
+#$sel->check_ok("archive", "Select need to be archived");
+#$sel->check_ok("ithelp", "Select need it help");
+#$sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address");
+#$sel->type_ok("description", "--- Bug created by Selenium ---", "Enter request description");
+#$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->is_text_present_ok('has been added to the database', 'Bug created');
+#my $brownbag_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# presentation
+
+#$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=mozilla.org&format=presentation");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->title_is("Mozilla Corporation Mountain View Presentation Request", "Open custom bug entry form - presentation");
+#$sel->type_ok("presenter", "Bugzilla Administrator", "Enter presenter");
+#$sel->type_ok("topic", "Automated testing of Bugzilla", "Enter topic");
+#$sel->type_ok("date", "01/01/2012", "Enter date");
+#$sel->select_ok("time_hour", "value=1", "Select hour");
+#$sel->select_ok("time_minute", "value=30", "Select minute");
+#$sel->select_ok("ampm", "value=PM", "Select am/pm");
+#$sel->select_ok("audience", "value=Employees Only", "Select audience");
+#$sel->check_ok("airmozilla", "Select need airmozilla");
+#$sel->check_ok("dialin", "Select need dial in");
+#$sel->check_ok("archive", "Select need to be archived");
+#$sel->check_ok("ithelp", "Select need it help");
+#$sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address");
+#$sel->type_ok("description", "--- Bug created by Selenium ---", "Enter request description");
+#$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->is_text_present_ok('has been added to the database', 'Bug created');
+#my $presentation_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+_check_component('mozilla.org', 'Discussion Forums');
+
+#mozlist
+
+_check_version('mozilla.org', 'other');
+_check_component('mozilla.org', 'Discussion Forums');
+
+$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=mozilla.org&format=mozlist");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Mozilla Discussion Forum", "Open custom bug entry form - mozlist");
+$sel->type_ok("listName", "test-list", "Enter name for mailing list");
+$sel->type_ok("listAdmin", $config->{'admin_user_login'}, "Enter list administator");
+$sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address");
+$sel->check_ok("name=groups", "value=infra", "Select private group");
+$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description");
+$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+my $mozlist_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+_check_product('Mozilla PR');
+_check_component('Mozilla PR', 'China - AMO');
+_check_group('mozilla-confidential');
+
+#mozpr
+
+_check_group('pr-private');
+
+#$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Mozilla PR&format=mozpr");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->title_is("Create a PR Request", "Open custom bug entry form - mozpr");
+#$sel->select_ok("location", "value=China", "Select location");
+#$sel->select_ok("component", "value=China - AMO", "Select component");
+#$sel->select_ok("fakecomp", "value=AMO", "Select fake component");
+#$sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address");
+#$sel->type_ok("short_desc", "Bug created by Selenium", "Enter bug summary");
+#$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description");
+#$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+#$sel->wait_for_page_to_load_ok(WAIT_TIME);
+#$sel->is_text_present_ok('has been added to the database', 'Bug created');
+#my $mozpr_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# legal
+
+_check_product('Legal');
+_check_component('Legal', 'Canonical');
+_check_component('Legal', 'Copyright');
+_check_group('mozilla-employee-confidential');
+
+$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Legal&format=legal");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Mozilla Corporation Legal Requests", "Open custom bug entry form - legal");
+$sel->select_ok("component", "value=Canonical", "Select request type");
+$sel->type_ok("short_desc", "Bug created by Selenium", "Enter request summary");
+$sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address");
+$sel->type_ok("otherparty", "Other party", "Enter other party");
+$sel->type_ok("busobj", "Business objective", "Enter business objective");
+$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter request description");
+$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+my $legal_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# poweredby
+
+_check_product('Websites', 'other');
+_check_component('Websites', 'www.mozilla.org');
+_check_user('liz@mozilla.com');
+
+$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Websites&format=poweredby");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Powered by Mozilla Logo Requests", "Open custom bug entry form - poweredby");
+$sel->type_ok("short_desc", "Bug created by Selenium", "Enter bug summary");
+$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description");
+$sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+my $poweredby_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} });
+logout($sel);
+
+sub _check_product {
+ my ($product, $version) = @_;
+
+ go_to_admin($sel);
+ $sel->click_ok("link=Products");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Select product");
+
+ my $product_description = "$product Description";
+
+ my $text = trim($sel->get_text("bugzilla-body"));
+ if ($text =~ /$product_description/) {
+ # Product exists already
+ return 1;
+ }
+
+ $sel->click_ok("link=Add");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Add Product");
+ $sel->type_ok("product", $product);
+ $sel->type_ok("description", $product_description);
+ $sel->type_ok("version", $version) if $version;
+ $sel->select_ok("default_op_sys_id", "Unspecified");
+ $sel->select_ok("default_platform_id", "Unspecified");
+ $sel->click_ok('//input[@type="submit" and @value="Add"]');
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $text = trim($sel->get_text("message"));
+ ok($text =~ /You will need to add at least one component before anyone can enter bugs against this product/,
+ "Display a reminder about missing components");
+
+ return 1;
+}
+
+sub _check_component {
+ my ($product, $component) = @_;
+
+ go_to_admin($sel);
+ $sel->click_ok("link=components");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Edit components for which product?");
+
+ $sel->click_ok("link=$product");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Select component of product '$product'");
+
+ my $component_description = "$component Description";
+
+ my $text = trim($sel->get_text("bugzilla-body"));
+ if ($text =~ /$component_description/) {
+ # Component exists already
+ return 1;
+ }
+
+ # Add the watch user for component watching
+ my $watch_user = lc $component . "@" . lc $product . ".bugs";
+ $watch_user =~ s/ & /-/;
+ $watch_user =~ s/\s+/\-/g;
+ $watch_user =~ s/://g;
+
+ go_to_admin($sel);
+ $sel->click_ok("link=components");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Edit components for which product?");
+ $sel->click_ok("link=$product");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Select component of product '$product'");
+ $sel->click_ok("link=Add");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Add component to the $product product");
+ $sel->type_ok("component", $component);
+ $sel->type_ok("description", $component_description);
+ $sel->type_ok("initialowner", $config->{'admin_user_login'});
+ $sel->uncheck_ok("watch_user_auto");
+ $sel->type_ok("watch_user", $watch_user);
+ $sel->check_ok("watch_user_auto");
+ $sel->click_ok('//input[@type="submit" and @value="Add"]');
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Component Created");
+ $text = trim($sel->get_text("message"));
+ ok($text eq "The component $component has been created.", "Component successfully created");
+
+ return 1;
+}
+
+sub _check_group {
+ my ($group) = @_;
+
+ go_to_admin($sel);
+ $sel->click_ok("link=Groups");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Edit Groups");
+
+ my $group_description = "$group Description";
+
+ my $text = trim($sel->get_text("bugzilla-body"));
+ if ($text =~ /$group_description/) {
+ # Group exists already
+ return 1;
+ }
+
+ $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", $group);
+ $sel->type_ok("desc", $group_description);
+ $sel->check_ok("isactive");
+ $sel->check_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");
+
+ return 1;
+}
+
+sub _check_version {
+ my ($product, $version) = @_;
+
+ go_to_admin($sel);
+ $sel->click_ok("link=versions");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Edit versions for which product?");
+ $sel->click_ok("link=$product");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+
+ my $text = trim($sel->get_text("bugzilla-body"));
+ if ($text =~ /$version/) {
+ # Version exists already
+ return 1;
+ }
+
+ $sel->click_ok("link=Add");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_like(qr/^Add Version to Product/);
+ $sel->type_ok("version", $version);
+ $sel->click_ok("create");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Version Created");
+
+ return 1;
+}
+
+sub _check_user {
+ my ($user) = @_;
+
+ go_to_admin($sel);
+ $sel->click_ok("link=Users");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Search users");
+ $sel->type_ok("matchstr", $user);
+ $sel->click_ok("search");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+
+ my $text = trim($sel->get_text("bugzilla-body"));
+ if ($text =~ /$user/) {
+ # User exists already
+ return 1;
+ }
+
+ $sel->click_ok("link=add a new user");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is('Add user');
+ $sel->type_ok('login', $user);
+ $sel->type_ok('password', 'password');
+ $sel->click_ok("add");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->is_text_present('regexp:The user account .* has been created successfully');
+
+ return 1;
+}
diff --git a/qa/t/test_bmo_retire_values.t b/qa/t/test_bmo_retire_values.t
new file mode 100644
index 000000000..f6086e0f9
--- /dev/null
+++ b/qa/t/test_bmo_retire_values.t
@@ -0,0 +1,388 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+my ($text, $bug_id);
+
+my $admin_user_login = $config->{admin_user_login};
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Bug Fields" => {"useclassification-off" => undef,
+ "usetargetmilestone-on" => undef},
+ "Administrative Policies" => {"allowbugdeletion-on" => undef},
+ });
+
+# create a clean bug
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "label=TestComponent");
+$sel->type_ok("short_desc", "testing testComponent");
+$sel->type_ok("comment", "testing");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $clean_bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $clean_bug_id created");
+
+#
+# component
+#
+# add a new component to TestProduct
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit components:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select component of product 'TestProduct'");
+$text = trim($sel->get_text("bugzilla-body"));
+if ($text =~ /TempComponent/) {
+ $sel->click_ok("//a[contains(\@href, 'editcomponents.cgi?action=del&product=TestProduct&component=TempComponent')]");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Delete component 'TempComponent' from 'TestProduct' product");
+ $sel->click_ok("delete");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Component Deleted");
+}
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add component to the TestProduct product");
+$sel->type_ok("component", "TempComponent");
+$sel->type_ok("description", "Temp component");
+$sel->type_ok("initialowner", $admin_user_login);
+$sel->uncheck_ok("watch_user_auto");
+$sel->type_ok("watch_user", 'tempcomponent@testproduct.bugs');
+$sel->check_ok("watch_user_auto");
+$sel->click_ok('//input[@type="submit" and @value="Add"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Created");
+
+# create bug into TempComponent
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("component", "label=TempComponent");
+$sel->type_ok("short_desc", "testing tempComponent");
+$sel->type_ok("comment", "testing");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug_id created");
+
+# disable TestProduct:TestComponent for bug entry
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit components:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select component of product 'TestProduct'");
+$sel->click_ok("link=TempComponent");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit component 'TempComponent' of product 'TestProduct'");
+$sel->click_ok("isactive");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Updated");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /Disabled for bugs/, "Component deactivation confirmed");
+
+# update bug TempComponent bug
+
+go_to_bug($sel, $bug_id);
+# make sure the component is still tempcomponent
+$sel->selected_label_is("component", 'TempComponent');
+# update
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug_id");
+$sel->click_ok("link=bug $bug_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+# make sure the component is still tempcomponent
+ok($sel->get_selected_labels("component"), 'TempComponent');
+
+# try creating new bug with TempComponent
+
+file_bug_in_product($sel, "TestProduct");
+ok(!$sel->is_element_present(
+ q#//select[@id='component']/option[@value='TempComponent']#),
+ 'TempComponent is missing from create');
+
+# try changing compoent of existing bug to TempComponent
+
+go_to_bug($sel, $clean_bug_id);
+ok(!$sel->is_element_present(
+ q#//select[@id='component']/option[@value='TempComponent']#),
+ 'TempComponent is missing from update');
+
+# delete TempComponent
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit components:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->click_ok("//a[contains(\@href, 'editcomponents.cgi?action=del&product=TestProduct&component=TempComponent')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete component 'TempComponent' from 'TestProduct' product");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Deleted");
+
+#
+# version
+#
+
+# add a new version to TestProduct
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit versions:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select version of product 'TestProduct'");
+$text = trim($sel->get_text("bugzilla-body"));
+if ($text =~ /TempVersion/) {
+ $sel->click_ok("//a[contains(\@href, 'editversions.cgi?action=del&product=TestProduct&version=TempVersion')]");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Delete Version of Product 'TestProduct'");
+ $sel->click_ok("delete");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Version Deleted");
+}
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Version to Product 'TestProduct'");
+$sel->type_ok("version", "TempVersion");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Version Created");
+
+# create bug with new version
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("version", "label=TempVersion");
+$sel->type_ok("short_desc", "testing tempVersion");
+$sel->type_ok("comment", "testing");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug_id created");
+
+# disable new version for bug entry
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit versions:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select version of product 'TestProduct'");
+$sel->click_ok("link=TempVersion");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Version 'TempVersion' of product 'TestProduct'");
+$sel->click_ok("isactive");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Version Updated");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /Disabled for bugs/, "Version deactivation confirmed");
+
+# update new version bug
+
+go_to_bug($sel, $bug_id);
+# make sure the version is still tempversion
+$sel->selected_label_is("version", 'TempVersion');
+# update
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug_id");
+$sel->click_ok("link=bug $bug_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+# make sure the version is still tempversion
+$sel->selected_label_is("version", 'TempVersion');
+# change the version so it can be deleted
+$sel->select_ok("version", "label=unspecified");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug_id");
+
+# try creating new bug with new version
+
+file_bug_in_product($sel, "TestProduct");
+ok(!$sel->is_element_present(
+ q#//select[@id='version']/option[@value='TempVersion']#),
+ 'TempVersion is missing from create');
+
+# try changing existing bug to new version
+
+go_to_bug($sel, $clean_bug_id);
+ok(!$sel->is_element_present(
+ q#//select[@id='version']/option[@value='TempVersion']#),
+ 'TempVersion is missing from update');
+
+# delete new version
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit versions:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select version of product 'TestProduct'");
+$text = trim($sel->get_text("bugzilla-body"));
+$sel->click_ok("//a[contains(\@href, 'editversions.cgi?action=del&product=TestProduct&version=TempVersion')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Version of Product 'TestProduct'");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Version Deleted");
+
+#
+# milestone
+#
+
+# add a milestone to TestProduct
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit milestones:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'");
+$text = trim($sel->get_text("bugzilla-body"));
+if ($text =~ /TempMilestone/) {
+ $sel->click_ok("//a[contains(\@href, 'editmilestones.cgi?action=del&product=TestProduct&milestone=TempMilestone')]");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Delete Milestone of Product 'TestProduct'");
+ $sel->click_ok("delete");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Milestone Deleted");
+}
+$sel->click_ok("link=Add");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add Milestone to Product 'TestProduct'");
+$sel->type_ok("milestone", "TempMilestone");
+$sel->type_ok("sortkey", "999");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Created");
+
+# create bug with milestone
+
+file_bug_in_product($sel, "TestProduct");
+$sel->select_ok("target_milestone", "label=TempMilestone");
+$sel->type_ok("short_desc", "testing tempMilestone");
+$sel->type_ok("comment", "testing");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug_id created");
+
+# disable milestone for bug entry
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit milestones:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'");
+$sel->click_ok("link=TempMilestone");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Milestone 'TempMilestone' of product 'TestProduct'");
+$sel->click_ok("isactive");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Updated");
+$text = trim($sel->get_text("bugzilla-body"));
+ok($text =~ /Disabled for bugs/, "Milestone deactivation confirmed");
+
+# update milestone bug
+
+go_to_bug($sel, $bug_id);
+# make sure the milestone is still tempmilestone
+$sel->selected_label_is("target_milestone", 'TempMilestone');
+# update
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug_id");
+$sel->click_ok("link=bug $bug_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+# make sure the milestone is still tempmilestone
+$sel->selected_label_is("target_milestone", 'TempMilestone');
+
+# try creating new bug with milestone
+
+file_bug_in_product($sel, "TestProduct");
+ok(!$sel->is_element_present(
+ q#//select[@id='target_milestone']/option[@value='TempMilestone']#),
+ 'TempMilestone is missing from create');
+
+# try changing existing bug to milestone
+
+go_to_bug($sel, $clean_bug_id);
+ok(!$sel->is_element_present(
+ q#//select[@id='target_milestone']/option[@value='TempMilestone']#),
+ 'TempMilestone is missing from update');
+
+# delete milestone
+
+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("link=TestProduct");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Product 'TestProduct'");
+$sel->click_ok("link=Edit milestones:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Select milestone of product 'TestProduct'");
+$text = trim($sel->get_text("bugzilla-body"));
+$sel->click_ok("//a[contains(\@href, 'editmilestones.cgi?action=del&product=TestProduct&milestone=TempMilestone')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Milestone of Product 'TestProduct'");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Milestone Deleted");
+
+logout($sel);
diff --git a/qa/t/test_bug_edit.t b/qa/t/test_bug_edit.t
new file mode 100644
index 000000000..64deafcf6
--- /dev/null
+++ b/qa/t/test_bug_edit.t
@@ -0,0 +1,504 @@
+# 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 strict;
+use warnings;
+use lib qw(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("link=Forget Search 'My bugs from QA_Selenium'");
+ $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');
+$sel->select_ok("bug_severity", "label=critical");
+$sel->type_ok("short_desc", "Test bug editing");
+$sel->type_ok("comment", "ploc");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+
+# 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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+# Now move the bug into another product, which has a mandatory group.
+
+$sel->click_ok("link=bug $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"]', "QA-Selenium-TEST group is selected");
+$sel->click_ok("change_product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->type_ok("comment", "I am the reporter, but I can see the bug anyway as I belong to the mandatory group");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+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!");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+$sel->click_ok("link=bug $bug1_id");
+$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", $config->{unprivileged_user_login});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+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("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"]', "Master group is editable");
+ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="Master"]'), "Master group not selected by default");
+$sel->click_ok("change_product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+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...");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+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->is_checked_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: QA 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');
+$sel->select_ok("bug_severity", "label=blocker");
+$sel->type_ok("short_desc", "New bug from me");
+# We turned on the CANEDIT bit for TestProduct.
+$sel->type_ok("comment", "I can enter a new bug, but not edit it, right?");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $bug2_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug2_id created");
+
+# Clicking the "Back" button and resubmitting the form again should trigger a suspicous action error.
+
+$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("you have no valid token for the create_bug action");
+$sel->click_ok('//input[@value="Confirm Changes"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', '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!");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+
+# 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("link=Change Several Bugs at Once");
+$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=bug $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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/$bug1_id /);
+$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}}
+# }
+# });
+#
+# # Mass-move has been removed, see 581690.
+# # Restore these tests once this bug is fixed.
+# # $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("link=Change Several Bugs at Once");
+# # $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("Bug $bug1_id has been moved to another database");
+# # $sel->is_text_present_ok("Bug $bug2_id has been moved to another database");
+# # $sel->click_ok("link=Bug $bug2_id");
+# # $sel->wait_for_page_to_load_ok(WAIT_TIME);
+# # $sel->title_like(qr/^$bug2_id/);
+# # $sel->selected_label_is("resolution", "MOVED");
+#
+# go_to_bug($sel, $bug2_id);
+# $sel->click_ok('oldbugmove');
+# $sel->wait_for_page_to_load_ok(WAIT_TIME);
+# $sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+# $sel->click_ok("link=bug $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");
+ $sel->click_ok("confirm");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+ $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($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 $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("link=Change Several Bugs at Once");
+$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("link=Forget Search 'My bugs from QA_Selenium'");
+$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/qa/t/test_choose_priority.t b/qa/t/test_choose_priority.t
new file mode 100644
index 000000000..a35823327
--- /dev/null
+++ b/qa/t/test_choose_priority.t
@@ -0,0 +1,27 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/test_classifications.t b/qa/t/test_classifications.t
new file mode 100644
index 000000000..ec5597d9e
--- /dev/null
+++ b/qa/t/test_classifications.t
@@ -0,0 +1,146 @@
+# 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 strict;
+use warnings;
+use lib qw(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");
+$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');
+$sel->type_ok("short_desc", "Bug in classification cone");
+$sel->type_ok("comment", "Created by Selenium with classifications turned on");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+
+# 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");
+
+# The classification the bug belongs to is no longer displayed since bug 452733.
+# Keeping the code here in case it comes back in a future release. :)
+# go_to_bug($sel, $bug1_id);
+# $sel->is_text_present_ok('[ctwo]');
+
+# 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/qa/t/test_config.t b/qa/t/test_config.t
new file mode 100644
index 000000000..e2f82fc21
--- /dev/null
+++ b/qa/t/test_config.t
@@ -0,0 +1,45 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/test_create_user_accounts.t b/qa/t/test_create_user_accounts.t
new file mode 100644
index 000000000..18b555aaf
--- /dev/null
+++ b/qa/t/test_create_user_accounts.t
@@ -0,0 +1,129 @@
+# 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 strict;
+use warnings;
+use lib qw(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->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('//input[@value="Create Account"]');
+$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('//input[@value="Create Account"]');
+$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 a while and try again/, "Too soon for this account");
+
+# These accounts do not pass the regexp.
+my @accounts = ('test@yahoo.com', 'test@bugzilla.net', 'test@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");
+ $sel->type_ok("login", $account);
+ $sel->click_ok('//input[@value="Create Account"]');
+ $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 and should cause a javascript alert.
+@accounts = qw(
+ test\bugzilla@bugzilla.test
+ testbugzilla.test
+ test@bugzilla
+ test@bugzilla.
+ 'test'@bugzilla.test
+ test&test@bugzilla.test
+ [test]@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");
+ $sel->type_ok("login", $account);
+ $sel->click_ok('//input[@value="Create Account"]');
+ ok($sel->get_alert() =~ /The e-mail address doesn't pass our syntax checking for a legal email address/,
+ '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('//input[@value="Create Account"]');
+$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/qa/t/test_custom_fields.t b/qa/t/test_custom_fields.t
new file mode 100644
index 000000000..7c69e4bbf
--- /dev/null
+++ b/qa/t/test_custom_fields.t
@@ -0,0 +1,459 @@
+# 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 strict;
+use warnings;
+use lib qw(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);
+$sel->type_ok("comment", "hops!");
+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);
+
+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);
+
+# 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("link=Change Several Bugs at Once");
+$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=bug $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);
+
+# 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->select_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);
+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);
+
+go_to_bug($sel, $bug1_id);
+$sel->select_ok("bug_status", "VERIFIED");
+$sel->select_ok("resolution", "INVALID");
+edit_bug($sel, $bug1_id);
+
+# 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/qa/t/test_custom_fields_admin.t b/qa/t/test_custom_fields_admin.t
new file mode 100644
index 000000000..cadb6c157
--- /dev/null
+++ b/qa/t/test_custom_fields_admin.t
@@ -0,0 +1,53 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/test_default_groups.t b/qa/t/test_default_groups.t
new file mode 100644
index 000000000..9875bc847
--- /dev/null
+++ b/qa/t/test_default_groups.t
@@ -0,0 +1,210 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Turn on the makeproductgroups parameter. Create a new product and check that
+# it has automatically a group created for it with the same name.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, { "Group Security" => {"makeproductgroups-on" => undef} });
+add_product($sel);
+$sel->type_ok("product", "ready_to_die");
+$sel->type_ok("description", "will die");
+$sel->select_ok("default_op_sys_id", "Unspecified");
+$sel->select_ok("default_platform_id", "Unspecified");
+$sel->click_ok('//input[@value="Add"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Created");
+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=ready_to_die");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Group: ready_to_die");
+my $group_url = $sel->get_location();
+$group_url =~ /group=(\d+)/;
+my $group1_id = $1;
+$sel->value_is("desc", "Access to bugs in the ready_to_die product");
+my @groups = $sel->get_select_options("members_remove");
+ok((grep { $_ eq 'admin' } @groups), "'admin' inherits group membership");
+@groups = $sel->get_select_options("bless_from_remove");
+ok((grep { $_ eq 'admin' } @groups), "'admin' inherits can bless group membership");
+$sel->is_checked_ok("isactive");
+
+# Check that the automatically created product group has the membercontrol
+# for it set to Default and othercontrol set to NA.
+
+edit_product($sel, "ready_to_die");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Group Controls for ready_to_die");
+$sel->value_is("entry_$group1_id", "off");
+$sel->value_is("canedit_$group1_id", "off");
+$sel->selected_label_is("membercontrol_$group1_id", "Default");
+$sel->selected_label_is("othercontrol_$group1_id", "NA");
+
+edit_product($sel, "ready_to_die");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->click_ok('//a[@href="editproducts.cgi?action=del&product=ready_to_die"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Product 'ready_to_die'");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Deleted");
+
+# The product has been deleted, but the group must survive.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Groups");
+$sel->is_text_present_ok("Access to bugs in the ready_to_die product");
+
+# Create a new product. As the "ready_to_die" group already exists,
+# a new "ready_to_die_" one must be created.
+
+add_product($sel);
+$sel->type_ok("product", "ready_to_die");
+$sel->type_ok("description", "will die");
+$sel->select_ok("default_op_sys_id", "Unspecified");
+$sel->select_ok("default_platform_id", "Unspecified");
+$sel->click_ok('//input[@value="Add"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Created");
+
+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=ready_to_die_");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Change Group: ready_to_die_");
+$group_url = $sel->get_location();
+$group_url =~ /group=(\d+)/;
+my $group2_id = $1;
+$sel->value_is("desc", "Access to bugs in the ready_to_die product");
+@groups = $sel->get_select_options("members_remove");
+ok((grep { $_ eq 'admin' } @groups), "'admin' inherits group membership");
+@groups = $sel->get_select_options("bless_from_remove");
+ok((grep { $_ eq 'admin' } @groups), "'admin' inherits can bless group membership");
+$sel->value_is("isactive", "on");
+
+# Check group settings. The old 'ready_to_die' group has no relationship
+# with this new product, despite its identical name.
+
+edit_product($sel, "ready_to_die");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Group Controls for ready_to_die");
+$sel->value_is("entry_$group1_id", "off");
+$sel->value_is("entry_$group2_id", "off");
+$sel->value_is("canedit_$group1_id", "off");
+$sel->value_is("canedit_$group2_id", "off");
+$sel->selected_label_is("membercontrol_$group1_id", "NA");
+$sel->selected_label_is("othercontrol_$group1_id", "NA");
+$sel->selected_label_is("membercontrol_$group2_id", "Default");
+$sel->selected_label_is("othercontrol_$group2_id", "NA");
+
+# Delete the ready_to_die_ group. It's bound to the ready_to_die product,
+# so the deletion requires explicit agreement from the admin.
+
+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("//a[contains(\@href, 'editgroups.cgi?action=del&group=$group2_id')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete group");
+$sel->is_text_present_ok("This group is tied to the following products");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Cannot Delete Group");
+my $text = trim($sel->get_text("error_msg"));
+ok($text =~ qr/All references to this group must be removed/,
+ "Group ready_to_die_ cannot be deleted as it is bound to a product");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete group");
+$sel->click_ok("unbind");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Group Deleted");
+$text = trim($sel->get_text("message"));
+ok($text =~ qr/The group ready_to_die_ has been deleted/, "Group ready_to_die_ has been deleted");
+
+edit_product($sel, "ready_to_die");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->click_ok('//a[@href="editproducts.cgi?action=del&product=ready_to_die"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Product 'ready_to_die'");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Deleted");
+
+# Reset the makeproductgroups parameter. Now creating a new product must
+# not create a new group, nor bind any group with it.
+
+set_parameters($sel, { "Group Security" => {"makeproductgroups-off" => undef} });
+add_product($sel);
+$sel->type_ok("product", "ready_to_die");
+$sel->type_ok("description", "will die");
+$sel->select_ok("default_op_sys_id", "Unspecified");
+$sel->select_ok("default_platform_id", "Unspecified");
+$sel->click_ok('//input[@value="Add"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Created");
+
+# Make sure that all group controls are set to NA for this product.
+
+edit_product($sel, "ready_to_die");
+$sel->title_is("Edit Product 'ready_to_die'");
+$sel->click_ok("link=Edit Group Access Controls:");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Group Controls for ready_to_die");
+$sel->value_is("entry_$group1_id", "off");
+$sel->value_is("canedit_$group1_id", "off");
+$sel->selected_label_is("membercontrol_$group1_id", "NA");
+$sel->selected_label_is("othercontrol_$group1_id", "NA");
+
+# Delete remaining groups and products.
+
+go_to_admin($sel);
+$sel->click_ok("link=Groups");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Edit Groups");
+ok(!$sel->is_text_present('ready_to_die__'), 'No ready_to_die__ group created');
+$sel->click_ok("//a[contains(\@href, 'editgroups.cgi?action=del&group=$group1_id')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete group");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Group Deleted");
+$text = trim($sel->get_text("message"));
+ok($text =~ /The group ready_to_die has been deleted/, "Group ready_to_die has been deleted");
+
+edit_product($sel, "ready_to_die");
+$sel->go_back_ok();
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->click_ok('//a[@href="editproducts.cgi?action=del&product=ready_to_die"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Delete Product 'ready_to_die'");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Deleted");
+logout($sel);
diff --git a/qa/t/test_dependencies.t b/qa/t/test_dependencies.t
new file mode 100644
index 000000000..f842b5fcd
--- /dev/null
+++ b/qa/t/test_dependencies.t
@@ -0,0 +1,53 @@
+# 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 strict;
+use warnings;
+use lib qw(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);
+logout($sel);
diff --git a/qa/t/test_edit_products_properties.t b/qa/t/test_edit_products_properties.t
new file mode 100644
index 000000000..7b3eabc19
--- /dev/null
+++ b/qa/t/test_edit_products_properties.t
@@ -0,0 +1,361 @@
+# 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 strict;
+use warnings;
+use lib qw(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->select_ok("default_op_sys_id", "Unspecified");
+$sel->select_ok("default_platform_id", "Unspecified");
+$sel->click_ok('//input[@type="submit" and @value="Add"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$text = trim($sel->get_text("message"));
+ok($text =~ /You will need to add at least one component before anyone can enter bugs against this product/,
+ "Display a reminder about missing components");
+$sel->click_ok("link=add at least one component");
+$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 1");
+$sel->type_ok("initialowner", $admin_user_login);
+$sel->uncheck_ok("watch_user_auto");
+$sel->type_ok("watch_user", "first-comp\@kill-me.bugs");
+$sel->check_ok("watch_user_auto");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Created");
+$text = trim($sel->get_text("message"));
+ok($text eq 'The component first comp has been created.', "Component successfully created");
+
+# Try creating a second component with the same name.
+
+$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->uncheck_ok("watch_user_auto");
+$sel->type_ok("watch_user", "first-comp\@kill-me.bugs");
+$sel->check_ok("watch_user_auto");
+$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->uncheck_ok("watch_user_auto");
+$sel->type_ok("watch_user", "second-comp\@kill-me.bugs");
+$sel->check_ok("watch_user_auto");
+$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("//a[contains(text(),'Edit\nversions:')]");
+$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");
+$sel->type_ok("short_desc", "test create/edit product properties");
+$sel->type_ok("comment", "this bug will soon be dead");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+my $bug1_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+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");
+$sel->type_ok("short_desc", "check default CC list");
+$sel->type_ok("comment", "is the CC list populated correctly?");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+@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('//input[@type="submit" and @value="Save Changes"]');
+$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('//input[@type="submit" and @value="Save Changes"]');
+ $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('//input[@type="submit" and @value="Save Changes"]');
+ $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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/$bug1_id /);
+@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("//a[contains(text(),'Edit\nversions:')]");
+$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 bugs outstanding 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");
+$sel->type_ok("short_desc", "bye bye everybody!");
+$sel->type_ok("comment", "I'm dead :(");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+
+# 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/qa/t/test_email_preferences.t b/qa/t/test_email_preferences.t
new file mode 100644
index 000000000..cc314ea2c
--- /dev/null
+++ b/qa/t/test_email_preferences.t
@@ -0,0 +1,400 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# Used to test sent bugmails
+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});
+
+# 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("User Preferences");
+$sel->click_ok("link=Email Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Email Preferences");
+$sel->click_ok("//input[\@value='Disable All Bugmail']");
+$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 preferences 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->check_ok("post_bug_submit_action-enabled");
+$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 (by directly going to Email Prefs pane)
+logout($sel);
+log_in($sel, $config, 'editbugs');
+$sel->open_ok("$config->{bugzilla_installation}/userprefs.cgi?tab=email");
+$sel->is_text_present_ok("Email Preferences");
+$sel->click_ok("//input[\@value='Enable All Bugmail']");
+$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 preferences have been saved.");
+
+# Always show email recipients
+ok($sel->create_cookie('show_bugmail_recipients=1'), 'Always show recipient list');
+
+# Create a test bug (bugmail to both normal user and admin)
+file_bug_in_product($sel, "Another Product");
+$sel->select_ok("component", "label=c1");
+$sel->type_ok("short_desc", "Selenium Email Preference test bug", "Enter bug summary");
+$sel->type_ok("comment", "Created by Selenium to test Email Preferences", "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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug1_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+my @email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail");
+
+# 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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_normal, "Normal user got bugmail");
+# 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.)");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+ok($email_sentto[0] eq "no one", "No bugmail sent");
+# 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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail");
+# Request a flag from admin (bugmail to no one, request mail to no one)
+$sel->select_ok("flag_type-4", "label=?");
+$sel->type_ok("requestee_type-4", $config->{admin_user_login});
+$sel->value_is("requestee_type-4", $config->{admin_user_login});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+ok($email_sentto[0] eq "no one", "No bugmail sent");
+
+# 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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_normal, "Normal user got bugmail");
+# 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.)");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_normal, "Normal user got bugmail");
+# 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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail");
+# 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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail");
+# Request a flag from normal user (bugmail to admin but not normal user and request mail to admin)
+$sel->select_ok("flag_type-4", "label=?");
+$sel->type_ok("requestee_type-4", $config->{editbugs_user_login});
+$sel->value_is("requestee_type-4", $config->{editbugs_user_login});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail");
+# 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}, "?", "+");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail");
+
+# 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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail");
+# 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.)");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail");
+# 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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+@email_sentto = get_email_sentto($sel);
+is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail");
+# 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}, "?", "-");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+@email_sentto = get_email_sentto($sel);
+ok($email_sentto[0] eq "no one", "No bugmail sent");
+# Cancel both flags (bugmail and request mail to no one)
+set_flag($sel, undef, "+", "X", $flag1_id);
+set_flag($sel, undef, "-", "X", $flag2_id);
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+@email_sentto = get_email_sentto($sel);
+ok($email_sentto[0] eq "no one", "No bugmail sent");
+logout($sel);
+
+# Help functions
+sub get_email_sentto {
+ my ($sel) = @_;
+ my @email_sentto;
+ my $index = 1;
+ while ($sel->is_element_present("//dt[text()='Email sent to:']/following-sibling::dd/code[$index]")) {
+ push(@email_sentto,
+ $sel->get_text("//dt[text()='Email sent to:']/following-sibling::dd/code[$index]"));
+ $index++;
+ }
+ return ("no one") if !@email_sentto;
+ return sort @email_sentto;
+}
+
+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/qa/t/test_enter_new_bug.t b/qa/t/test_enter_new_bug.t
new file mode 100644
index 000000000..b7ac92461
--- /dev/null
+++ b/qa/t/test_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 strict;
+use warnings;
+use lib qw(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.
+
+foreach my $user (qw(admin unprivileged canconfirm)) {
+ log_in($sel, $config, $user);
+ file_bug_in_product($sel, "TestProduct");
+ $sel->type_ok("short_desc", "Bug created by Selenium",
+ "Enter bug summary");
+ $sel->type_ok("comment", "--- Bug created by Selenium ---",
+ "Enter bug description");
+ $sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->is_text_present_ok('has been added to the database', 'Bug created');
+ logout($sel);
+}
diff --git a/qa/t/test_flags.t b/qa/t/test_flags.t
new file mode 100644
index 000000000..0f7c950f7
--- /dev/null
+++ b/qa/t/test_flags.t
@@ -0,0 +1,482 @@
+# 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 strict;
+use warnings;
+use lib qw(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');
+$sel->type_ok("short_desc", "test flags");
+$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"]');
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+
+# All 3 bug flag types must be available; we are in the TestProduct product.
+
+$sel->click_ok("link=Bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id .* test flags/);
+$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 ?");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+
+# We need to store the new flag IDs.
+
+$sel->is_text_present_ok("$config->{admin_user_nick}: 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_nick}: 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_nick}: 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=-");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+
+# 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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+
+# 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=-");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+
+# 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");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('regexp:Attachment #\d+ to bug \d+ created');
+
+# 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("//a[contains(text(),'Create\n 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");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('regexp:Attachment #\d+ to bug \d+ created');
+$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("//a[contains(text(),'Create\n 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");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('regexp:Attachment #\d+ to bug \d+ created');
+$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_nick}: SeleniumAttachmentFlag1Test? ($config->{admin_user_nick})");
+$sel->is_text_present_ok("$config->{admin_user_nick}: SeleniumAttachmentFlag2Test?");
+$sel->is_text_present_ok("$config->{admin_user_nick}: 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"]');
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+
+# 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/);
+$sel->is_element_present_ok('//select[@title="attachmentflag2"][@disabled]',
+ "Attachment flags are not editable by a powerless user");
+
+# Add an attachment and set flags on it.
+
+$sel->click_ok("link=Bug $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", "on");
+
+# canconfirm/editbugs privs are required to edit this flag.
+
+ok(!$sel->is_editable("flag_type-$aflagtype1_id"), "Flag type non editable by powerless users");
+
+# No privs are required to edit this flag.
+
+$sel->select_ok("flag_type-$aflagtype2_id", "label=+");
+$sel->type_ok("comment", "granting again");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('regexp:Attachment #\d+ to bug \d+ created');
+$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->{unprivileged_user_nick}: 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=+");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes to attachment $attachment3_id of bug $bug1_id submitted");
+
+# 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/qa/t/test_flags2.t b/qa/t/test_flags2.t
new file mode 100644
index 000000000..9706af639
--- /dev/null
+++ b/qa/t/test_flags2.t
@@ -0,0 +1,344 @@
+# 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 strict;
+use warnings;
+use lib qw(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->click_ok('//input[@value="Set bug flags"]');
+$sel->select_ok("flag_type-$flagtype1_id", "label=+");
+$sel->type_ok("short_desc", "The selenium flag should be kept on product change");
+$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->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=-");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+
+# Store the bug and flag IDs.
+
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$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_nick}: 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_nick}: 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");
+$sel->click_ok("change_product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->selected_label_is("flag-$flag1_id", "+");
+$sel->is_text_present_ok("$config->{admin_user_nick}: 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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+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_nick}: 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->click_ok('//input[@value="Set bug flags"]');
+$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=?");
+$sel->type_ok("short_desc", "Create a new selenium flag for c2");
+$sel->type_ok("comment", ".");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+
+# Store the bug and flag IDs.
+
+my $bug2_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->click_ok("link=Bug $bug2_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug2_id /);
+$sel->is_text_present_ok("$config->{admin_user_nick}: 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");
+$sel->type_ok("comment", "The selenium flag should be preserved.");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+$sel->click_ok("link=bug $bug2_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug2_id /);
+$sel->selected_label_is("flag-$flag2_id", '?');
+ok(!$sel->is_element_present("flag_type-$flagtype1_id"), "Flag type not available in 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=+");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+$sel->click_ok("link=bug $bug2_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug2_id /);
+$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...");
+$sel->click_ok("change_product");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+$sel->click_ok("link=bug $bug2_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$bug2_id /);
+ok(!$sel->is_element_present("flag-$flag2_id"), "Flag $flag2_id deleted");
+ok(!$sel->is_editable("flag_type-$flagtype1_id"), "Flag type 'selenium' not editable by powerless users");
+ok(!$sel->is_element_present("flag_type-$flagtype2_id"), "Flag type not available in 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/qa/t/test_groups.t b/qa/t/test_groups.t
new file mode 100644
index 000000000..1974091ea
--- /dev/null
+++ b/qa/t/test_groups.t
@@ -0,0 +1,323 @@
+# 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 strict;
+use warnings;
+use lib qw(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}");
+$sel->type_ok("short_desc", "bug restricted to the Selenium group");
+$sel->type_ok("comment", "should be invisible");
+$sel->selected_label_is("component", "TestComponent");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+$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");
+$sel->type_ok("short_desc", "bug restricted to the Selenium group");
+$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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug2_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug2_id created");
+
+# 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("the group is newly 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\W+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");
+$sel->type_ok("short_desc", "Selenium-test group mandatory");
+$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)");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug3_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug3_id created");
+
+# 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");
+$sel->type_ok("short_desc", "bug restricted to the Selenium-test group");
+$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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug4_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug4_id created");
+
+# 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("the group is no longer applicable and will be removed");
+
+# 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_is("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("link=Forget Search 'Selenium bugs'");
+$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/qa/t/test_keywords.t b/qa/t/test_keywords.t
new file mode 100644
index 000000000..5789676b4
--- /dev/null
+++ b/qa/t/test_keywords.t
@@ -0,0 +1,176 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+
+my $test_bug_1 = $config->{test_bug_1};
+my $test_bug_2 = $config->{test_bug_2};
+
+# 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
+
+go_to_bug($sel, $test_bug_1);
+# If another script is playing with keywords too, don't mess with it.
+my $kw1 = $sel->get_text("keywords");
+$sel->type_ok("keywords", "$kw1, key-selenium-kone");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_1");
+go_to_bug($sel, $test_bug_2);
+my $kw2 = $sel->get_text("keywords");
+$sel->type_ok("keywords", "$kw2, key-selenium-kone, key-selenium-ktwo");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_2");
+
+# 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");
+# Bugzilla doesn't allow substrings for keywords.
+$sel->type_ok("keywords", "selen");
+$sel->click_ok("Search");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Zarro Boogs found");
+
+# Make sure describekeywords.cgi works as expected.
+
+$sel->open_ok("/$config->{bugzilla_installation}/describekeywords.cgi");
+$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=$test_bug_1");
+$sel->is_element_present_ok("link=$test_bug_2");
+$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=$test_bug_2");
+$sel->is_text_present_ok("One bug found");
+logout($sel);
diff --git a/qa/t/test_login.t b/qa/t/test_login.t
new file mode 100644
index 000000000..2b66d6e3c
--- /dev/null
+++ b/qa/t/test_login.t
@@ -0,0 +1,32 @@
+# 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 strict;
+use warnings;
+use lib qw(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 access an admin page.
+
+$sel->open_ok("/$config->{bugzilla_installation}/editparams.cgi");
+$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 Username Or Password");
+$sel->is_text_present_ok("The username or password you entered is not valid.");
diff --git a/qa/t/test_long_list_redirection.t b/qa/t/test_long_list_redirection.t
new file mode 100644
index 000000000..4e3a8ba97
--- /dev/null
+++ b/qa/t/test_long_list_redirection.t
@@ -0,0 +1,20 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+use Test::More "no_plan";
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+$sel->open_ok("/$config->{bugzilla_installation}/long_list.cgi?id=1");
+$sel->title_is("Full Text Bug Listing", "Display bug as format for printing");
+my $text = $sel->get_text("//h1");
+$text =~ s/[\r\n\t\s]+/ /g;
+is($text, 'Bug 1', 'Display bug 1 specifically');
diff --git a/qa/t/test_milestones.t b/qa/t/test_milestones.t
new file mode 100644
index 000000000..baf04d402
--- /dev/null
+++ b/qa/t/test_milestones.t
@@ -0,0 +1,170 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+# 1st step: turn on usetargetmilestone, musthavemilestoneonaccept and letsubmitterchoosemilestone.
+
+log_in($sel, $config, 'admin');
+set_parameters($sel, {'Bug Fields' => {'usetargetmilestone-on' => undef},
+ 'Bug Change Policies' => {'musthavemilestoneonaccept-on' => undef,
+ '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)");
+$sel->type_ok("short_desc", "Target Milestone left to default", "Enter bug summary");
+$sel->type_ok("comment", "Created by Selenium to test 'musthavemilestoneonaccept'", "Enter bug description");
+$sel->click_ok("commit", undef, "Commit bug data to post_bug.cgi");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug1_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+
+# 4th step: edit the bug (test musthavemilestoneonaccept ON).
+
+$sel->select_ok("bug_status", "label=IN_PROGRESS", "Change bug status to IN_PROGRESS");
+$sel->click_ok("commit", undef, "Save changes");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Milestone Required", "Change rejected: musthavemilestoneonaccept is on but the milestone selected is the default one");
+$sel->is_text_present_ok("You must select a target milestone", undef, "Display error message");
+# We cannot use go_back_ok() because we just left post_bug.cgi where data has been submitted using POST.
+go_to_bug($sel, $bug1_id);
+$sel->select_ok("target_milestone", "label=2.0", "Select a non-default milestone");
+$sel->click_ok("commit", undef, "Save changes (2nd attempt)");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+# 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)");
+$sel->type_ok("short_desc", "Target Milestone set to non-default", "Enter bug summary");
+$sel->type_ok("comment", "Created by Selenium to test 'musthavemilestoneonaccept'", "Enter bug description");
+$sel->click_ok("commit", undef, "Commit bug data to post_bug.cgi");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug2_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug2_id created");
+
+# 6th step: edit the bug (test musthavemilestoneonaccept ON).
+
+$sel->select_ok("bug_status", "label=IN_PROGRESS");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug2_id");
+
+
+# 7th step: test validation methods for milestones.
+
+$sel->open_ok("/$config->{bugzilla_installation}/editmilestones.cgi");
+$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.
+
+$sel->open_ok("/$config->{bugzilla_installation}/show_bug.cgi?id=$bug1_id");
+$sel->title_like(qr/^$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");
+$sel->type_ok("short_desc", "Only one Target Milestone available", "Enter bug summary");
+$sel->type_ok("comment", "Created by Selenium to test 'musthavemilestoneonaccept'", "Enter bug description");
+$sel->click_ok("commit", undef, "Commit bug data to post_bug.cgi");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug3_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug3_id created");
+
+# 10th step: musthavemilestoneonaccept must have no effect as there is
+# no other milestone available besides the default one.
+
+$sel->select_ok("bug_status", "label=IN_PROGRESS");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug3_id");
+
+# 11th step: turn musthavemilestoneonaccept back to OFF.
+
+set_parameters($sel, {'Bug Change Policies' => {'musthavemilestoneonaccept-off' => undef}});
+logout($sel);
diff --git a/qa/t/test_private_attachments.t b/qa/t/test_private_attachments.t
new file mode 100644
index 000000000..74b5adac9
--- /dev/null
+++ b/qa/t/test_private_attachments.t
@@ -0,0 +1,200 @@
+# 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 strict;
+use warnings;
+use lib qw(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");
+$sel->type_ok("short_desc", "Some comments are private");
+$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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$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");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('regexp:Attachment #\d+ to bug \d+ created');
+
+# We need to store the attachment ID.
+
+my $alink = $sel->get_attribute('//a[@title="public attachment, v2"]@href');
+$alink =~ /id=(\d+)/;
+my $attachment1_id = $1;
+
+# Be sure to redisplay the same bug, and make sure the new attachment is visible.
+
+$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("public attachment, v2");
+$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 QA Admin");
+$sel->type_ok("comment", "This attachment is not mine.");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes to attachment $attachment1_id of bug $bug1_id submitted");
+$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("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.
+$sel->is_text_present_ok("[no attachments can be made obsolete]");
+$sel->type_ok("description", "My patch, which I should see, always");
+$sel->type_ok("comment", "This is my patch!");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('regexp:Attachment #\d+ to bug \d+ created');
+$alink = $sel->get_attribute('//a[@title="My patch, which I should see, always"]@href');
+$alink =~ /id=(\d+)/;
+my $attachment2_id = $1;
+$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("My patch, which I should see, always (");
+$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.");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes to attachment $attachment2_id of bug $bug1_id submitted");
+$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("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");
+$sel->click_ok("delete");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes to attachment $attachment2_id of bug $bug1_id submitted");
+$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("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/qa/t/test_qa_contact.t b/qa/t/test_qa_contact.t
new file mode 100644
index 000000000..1de131861
--- /dev/null
+++ b/qa/t/test_qa_contact.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.
+
+use strict;
+use warnings;
+use lib qw(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->open_ok("/$config->{bugzilla_installation}/buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=My%20QA%20query",
+ undef, "Make sure the 'My QA query' saved search isn't present");
+ # We bypass the UI to delete the saved search, and so Bugzilla should complain about the missing token.
+ $sel->title_is("Suspicious Action");
+ $sel->is_text_present_ok("It looks like you didn't come from the right page");
+ $sel->click_ok("confirm");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Search is gone");
+ my $text = trim($sel->get_text("message"));
+ ok($text =~ /OK, the My QA query search is gone/, "Removed the 'My QA query' saved search");
+}
+
+# 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");
+$sel->type_ok("short_desc", "Test for QA contact");
+$sel->type_ok("comment", "This is a test to check QA contact privs.");
+$sel->check_ok('//input[@name="groups" and @value="Master"]');
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database', 'Bug created');
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# 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_element_present('//label[@for="qa_contact"]'));
+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("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("//a[contains(text(),'log\n 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("I need an email address 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("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. Before doing that, we need to set
+# some user prefs correctly to not interfere with our test.
+
+log_in($sel, $config, 'unprivileged');
+$sel->click_ok("link=Preferences");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+$sel->select_ok("state_addselfcc", "value=never");
+$sel->select_ok("post_bug_submit_action", "value=same_bug");
+$sel->click_ok("update");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User 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");
+$sel->click_ok("commit");
+
+# The user is no longer the QA contact, and he has no other role
+# with the bug. He can no longer see it.
+
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("(list of e-mails not available)");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Access Denied");
+logout($sel);
diff --git a/qa/t/test_require_login.t b/qa/t/test_require_login.t
new file mode 100644
index 000000000..e4fd81891
--- /dev/null
+++ b/qa/t/test_require_login.t
@@ -0,0 +1,81 @@
+# 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 strict;
+use warnings;
+use lib qw(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 long_list page
+ post_bug process_bug query quips report reports request
+ sanitycheck search_plugin show_activity show_bug showattachment
+ showdependencygraph showdependencytree summarize_time
+ userprefs votes xml);
+
+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/qa/t/test_sanity_check.t b/qa/t/test_sanity_check.t
new file mode 100644
index 000000000..84fc4d8c4
--- /dev/null
+++ b/qa/t/test_sanity_check.t
@@ -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.
+
+use strict;
+use warnings;
+use lib qw(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/qa/t/test_saved_searches.t b/qa/t/test_saved_searches.t
new file mode 100644
index 000000000..b92307e60
--- /dev/null
+++ b/qa/t/test_saved_searches.t
@@ -0,0 +1,114 @@
+# 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 strict;
+use warnings;
+use lib qw(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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+
+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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+
+$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("User Preferences");
+$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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+
+$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("link=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("link=Forget Search 'SavedSearchTEST1'");
+$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/qa/t/test_search.t b/qa/t/test_search.t
new file mode 100644
index 000000000..b1f77aebf
--- /dev/null
+++ b/qa/t/test_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 strict;
+use warnings;
+use lib qw(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("editme_action");
+$bug_summary .= ": my ID is $bug1_id";
+$sel->type_ok("short_desc", $bug_summary);
+$sel->type_ok("comment", "Updating bug summary....");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+# 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->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Search for bugs");
+$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/qa/t/test_security.t b/qa/t/test_security.t
new file mode 100644
index 000000000..42ec304e0
--- /dev/null
+++ b/qa/t/test_security.t
@@ -0,0 +1,177 @@
+# 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 strict;
+use warnings;
+use lib qw(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");
+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=simple patch, v1");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("");
+my @cookies = split(/[\s;]+/, $sel->get_cookie());
+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=simple patch, v1");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("");
+@cookies = split(/[\s;]+/, $sel->get_cookie());
+$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("User 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("User 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("User 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: QA 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);
+$sel->title_like(qr/Attachment \d+ Details for Bug $bug1_id/);
+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/qa/t/test_shared_searches.t b/qa/t/test_shared_searches.t
new file mode 100644
index 000000000..6b38a271b
--- /dev/null
+++ b/qa/t/test_shared_searches.t
@@ -0,0 +1,196 @@
+# 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 strict;
+use warnings;
+use lib qw(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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+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("User Preferences");
+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_text_present("Forget Search 'Shared Selenium buglist'"), "'Forget...' link 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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+$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("User Preferences");
+# 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("User Preferences");
+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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+$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("User Preferences");
+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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+$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("User Preferences");
+# 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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+# 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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+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("User Preferences");
+$sel->click_ok("link=Saved Searches");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+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/qa/t/test_show_all_products.t b/qa/t/test_show_all_products.t
new file mode 100644
index 000000000..97d915174
--- /dev/null
+++ b/qa/t/test_show_all_products.t
@@ -0,0 +1,57 @@
+# 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 strict;
+use warnings;
+use lib qw(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->title_is("Enter Bug");
+$sel->click_ok("link=Other Products", undef, "Choose full product list");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Enter Bug");
+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->title_is("Enter A Bug");
+if ($sel->is_text_present('None of the above; my bug is in')) {
+ $sel->click_ok('advanced_link');
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Enter Bug");
+}
+$sel->click_ok('link=Other Products');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("QA-Selenium-TEST");
+# For some unknown reason, Selenium doesn't like hyphens in links.
+# $sel->click_ok("link=QA-Selenium-TEST");
+$sel->click_ok('//div[@id="choose_product"]//a[contains(@href, "QA-Selenium-TEST")]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Product: QA-Selenium-TEST");
+logout($sel);
diff --git a/qa/t/test_shutdown.t b/qa/t/test_shutdown.t
new file mode 100644
index 000000000..be562ad24
--- /dev/null
+++ b/qa/t/test_shutdown.t
@@ -0,0 +1,75 @@
+# 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 strict;
+use warnings;
+use lib qw(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 long_list page post_bug process_bug query quips
+ relogin report reports request sanitycheck search_plugin
+ show_activity show_bug showattachment showdependencygraph
+ showdependencytree sidebar summarize_time token userprefs votes
+ xml 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/qa/t/test_status_whiteboard.t b/qa/t/test_status_whiteboard.t
new file mode 100644
index 000000000..6c7f7b085
--- /dev/null
+++ b/qa/t/test_status_whiteboard.t
@@ -0,0 +1,98 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+my $test_bug_1 = $config->{test_bug_1};
+my $test_bug_2 = $config->{test_bug_2};
+
+# Turn on usestatuswhiteboard
+
+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.
+
+$sel->open_ok("/$config->{bugzilla_installation}/show_bug.cgi?id=$test_bug_1");
+$sel->title_like(qr/^$test_bug_1\b/);
+$sel->is_text_present_ok("Whiteboard:");
+$sel->type_ok("status_whiteboard", "[msg from test_status_whiteboard.t: x77v]");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_1");
+$sel->open_ok("/$config->{bugzilla_installation}/show_bug.cgi?id=$test_bug_2");
+$sel->title_like(qr/^$test_bug_2\b/);
+$sel->type_ok("status_whiteboard", "[msg from test_status_whiteboard.t: x77v]");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_2");
+
+# 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}});
+# Show detailed bug information panel on advanced search
+ok($sel->create_cookie('TUI=information_query=1'), 'Show detailed bug information');
+$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");
+$sel->open_ok("/$config->{bugzilla_installation}/show_bug.cgi?id=$test_bug_1");
+$sel->title_like(qr/^$test_bug_1\b/);
+ok(!$sel->is_element_present('//label[@for="status_whiteboard"]'));
+
+# 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");
+$sel->click_ok("link=Forget Search 'sw-x77v'");
+$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.");
+
+# 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}});
+logout($sel);
diff --git a/qa/t/test_strict_isolation.t b/qa/t/test_strict_isolation.t
new file mode 100644
index 000000000..48ef222b3
--- /dev/null
+++ b/qa/t/test_strict_isolation.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 strict;
+use warnings;
+use lib qw(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_//;
+$sel->click_ok('commit');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+
+# 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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+go_to_bug($sel, $bug1_id);
+$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");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+# 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);
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+go_to_bug($sel, $bug1_id);
+$sel->click_ok("cc_edit_area_showhide");
+$sel->add_selection_ok("cc", "label=$qa_user");
+$sel->check_ok("removecc");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+# The powerless user still cannot be CC'ed.
+
+go_to_bug($sel, $bug1_id);
+$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/qa/t/test_sudo_sessions.t b/qa/t/test_sudo_sessions.t
new file mode 100644
index 000000000..828f41330
--- /dev/null
+++ b/qa/t/test_sudo_sessions.t
@@ -0,0 +1,143 @@
+# 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 strict;
+use warnings;
+use lib qw(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("Bugzilla_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("Bugzilla_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("User Preferences");
+$sel->click_ok("link=Permissions");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("User Preferences");
+$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 sudo session impersonating " . $config->{unprivileged_user_login});
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Sudo session complete");
+$sel->is_text_present_ok("The sudo session has been ended");
+
+# Try to access the sudo page directly, with no credentials.
+
+$sel->open_ok("/$config->{bugzilla_installation}/relogin.cgi?action=begin-sudo");
+$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&Bugzilla_login=$config->{admin_user_login}&Bugzilla_password=$config->{admin_user_passwd}&target_login=$config->{admin_user_login}", undef, "Impersonate a user directly by providing all required data");
+$sel->title_is("Preparation Required");
+
+# The link should populate the target_login field correctly.
+# Note that we are trying to sudo an admin, which is not allowed.
+
+$sel->click_ok("link=start your session normally");
+$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("Bugzilla_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");
+$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("Invalid Username Or Password");
+
+# 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("Bugzilla_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/qa/t/test_target_milestones.t b/qa/t/test_target_milestones.t
new file mode 100644
index 000000000..f02eb42d7
--- /dev/null
+++ b/qa/t/test_target_milestones.t
@@ -0,0 +1,109 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+my $test_bug_1 = $config->{test_bug_1};
+
+# Enable target milestones.
+
+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 test_bug_1.
+
+go_to_bug($sel, $test_bug_1);
+$sel->is_text_present_ok("Target Milestone:");
+$sel->select_ok("target_milestone", "label=TM1");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_1");
+
+# 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 Milestone:"), "The target milestone field is no longer displayed");
+
+go_to_bug($sel, $test_bug_1);
+ok(!$sel->is_element_present('//label[@for="target_milestone"]'));
+
+# 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("link=Forget Search 'selenium_m0'");
+$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/qa/t/test_time_summary.t b/qa/t/test_time_summary.t
new file mode 100644
index 000000000..0ae2ea5da
--- /dev/null
+++ b/qa/t/test_time_summary.t
@@ -0,0 +1,100 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+my $test_bug_1 = $config->{test_bug_1};
+
+# 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.
+
+go_to_bug($sel, $test_bug_1);
+$sel->type_ok("work_time", 2.6);
+$sel->type_ok("comment", "I did some work");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_1");
+# Make sure the correct bug is redisplayed.
+$sel->click_ok("link=bug $test_bug_1");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_like(qr/^$test_bug_1/, "Display bug $test_bug_1");
+$sel->is_text_present_ok("I did some work");
+$sel->is_text_present_ok("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 to view/, "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, $test_bug_1);
+$sel->click_ok("//a[contains(text(),'Summarize time')]");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Time Summary for Bug $test_bug_1");
+$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 $test_bug_1");
+logout($sel);
diff --git a/qa/t/test_user_groups.t b/qa/t/test_user_groups.t
new file mode 100644
index 000000000..e4dd5dc30
--- /dev/null
+++ b/qa/t/test_user_groups.t
@@ -0,0 +1,229 @@
+# 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 strict;
+use warnings;
+use lib qw(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("//a[contains(text(),'add\n 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("//a[contains(text(),'add\n 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>');
+
+# 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->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->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->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->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->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");
+ $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/qa/t/test_user_matching.t b/qa/t/test_user_matching.t
new file mode 100644
index 000000000..0326b8770
--- /dev/null
+++ b/qa/t/test_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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+
+my $test_bug_1 = $config->{test_bug_1};
+
+
+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}
+ });
+
+go_to_bug($sel, $test_bug_1);
+$sel->click_ok("cc_edit_area_showhide");
+
+# We enter an incomplete email address. process_bug.cgi must ask
+# for confirmation as confirmuniqueusermatch is turned on.
+
+$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/^$test_bug_1/);
+$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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_1");
+
+# Now test wildcards ("*"). Due to confirmuniqueusermatch being turned on,
+# a confirmation page must be displayed.
+
+go_to_bug($sel, $test_bug_1);
+$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/^$test_bug_1/);
+$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, $test_bug_1);
+$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/^$test_bug_1/);
+$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});
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $test_bug_1");
+
+# 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, $test_bug_1);
+$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/^$test_bug_1/);
+$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/^$test_bug_1/);
+$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, $test_bug_1);
+$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/qa/t/test_user_preferences.t b/qa/t/test_user_preferences.t
new file mode 100644
index 000000000..d4161a6f6
--- /dev/null
+++ b/qa/t/test_user_preferences.t
@@ -0,0 +1,237 @@
+# 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 strict;
+use warnings;
+use lib qw(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->check_ok("per_bug_queries-enabled");
+$sel->select_ok("per_bug_queries", "label=On");
+$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("User 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)");
+$sel->select_ok("per_bug_queries", "label=Site Default (On)");
+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("User 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", "");
+$sel->type_ok("short_desc", "First bug created");
+$sel->type_ok("comment", "I'm not in the CC list.");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+$sel->value_is("addselfcc", "off");
+$sel->select_ok("bug_status", "label=IN_PROGRESS");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$sel->click_ok("editme_action");
+$sel->value_is("short_desc", "First bug created");
+$sel->value_is("addselfcc", "off");
+
+# Tag the bug.
+
+$sel->select_ok("lob_action", "label=Add");
+$sel->type_ok("lob_newqueryname", "sel-tmp");
+$sel->type_ok("bug_ids", $bug1_id);
+$sel->click_ok("commit_list_of_bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Tag Updated");
+my $text = trim($sel->get_text("message"));
+ok($text =~ /The 'sel-tmp' tag has been added to bug $bug1_id/, "Bug added to 'sel-tmp' tag");
+$sel->click_ok("link=sel-tmp");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Tags: 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", "");
+$sel->type_ok("short_desc", "My second bug");
+$sel->type_ok("comment", "Still not in the CC list");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+my $bug2_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+$sel->is_text_present_ok('has been added to the database', "Bug $bug2_id created");
+$sel->value_is("addselfcc", "off");
+
+# Add another bug to the tag.
+
+$sel->select_ok("lob_action", "label=Add");
+$sel->select_ok("lob_oldqueryname", "label=sel-tmp");
+$sel->type_ok("bug_ids", $bug2_id);
+$sel->click_ok("commit_list_of_bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Tag Updated");
+$text = trim($sel->get_text("message"));
+ok($text =~ /The 'sel-tmp' tag has been added to bug $bug2_id/, "Bug added to 'sel-tmp' tag");
+$sel->click_ok("link=sel-tmp");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("Tags: 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.");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+
+$sel->click_ok("editme_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 tag from all bugs.
+
+$sel->open_ok("/$config->{bugzilla_installation}/buglist.cgi?tag=sel-tmp", undef, "List 'sel-tmp' bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("Tags: sel-tmp");
+$sel->click_ok("link=$bug1_id");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id /);
+$sel->select_ok("lob_action", "label=Remove");
+$sel->select_ok("lob_oldqueryname", "label=sel-tmp");
+$sel->type_ok("bug_ids", $bug1_id);
+$sel->click_ok("commit_list_of_bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Tag Updated");
+$text = trim($sel->get_text("message"));
+ok($text =~ /The 'sel-tmp' tag has been removed from bug $bug1_id/, "Bug removed from 'sel-tmp' tag");
+$sel->click_ok("link=sel-tmp");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("Tags: sel-tmp");
+$sel->click_ok("link=$bug2_id");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^$bug2_id /);
+$sel->select_ok("lob_action", "label=Remove");
+$sel->select_ok("lob_oldqueryname", "label=sel-tmp");
+$sel->type_ok("bug_ids", $bug2_id);
+$sel->click_ok("commit_list_of_bugs");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Tag Updated");
+$text = trim($sel->get_text("message"));
+ok($text =~ /The 'sel-tmp' tag has been removed from bug $bug2_id/, "Bug removed from 'sel-tmp' tag");
+$sel->click_ok("link=sel-tmp");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Bug List");
+$sel->is_text_present_ok("Tags: sel-tmp");
+$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("User 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");
+$sel->select_ok("per_bug_queries", "label=Off");
+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("User Preferences");
+
+ok(!$sel->is_element_present("lob_action"), "Element 1/3 for tags is not displayed");
+ok(!$sel->is_element_present("lob_newqueryname"), "Element 2/3 for tags is not displayed");
+ok(!$sel->is_element_present("commit_list_of_bugs"), "Element 3/3 for tags is not displayed");
+
+# 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");
+$text = trim($sel->get_text("message"));
+ok($text =~ /OK, you have a new search named my_list./, "New saved search 'my_list' has been created");
+
+# 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 .* First bug created/);
+$sel->value_is("addselfcc", "on");
+$sel->type_ok("comment", "I should be CC'ed and then I should see the next bug.");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->is_text_present_ok("Changes submitted for bug $bug1_id");
+$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_ok("My second bug");
+$sel->value_is("addselfcc", "on");
+$sel->click_ok("link=bug $bug1_id");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_like(qr/^$bug1_id .* First bug created/);
+$sel->is_text_present_ok("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("link=Forget Search 'my_list'");
+$sel->wait_for_page_to_load(WAIT_TIME);
+$sel->title_is("Search is gone");
+$text = trim($sel->get_text("message"));
+ok($text =~ /OK, the my_list search is gone/, "The saved search 'my_list' has been deleted");
+logout($sel);
diff --git a/qa/t/test_user_privs.t b/qa/t/test_user_privs.t
new file mode 100644
index 000000000..367049bc7
--- /dev/null
+++ b/qa/t/test_user_privs.t
@@ -0,0 +1,68 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+
+use Test::More "no_plan";
+
+use QA::Util;
+
+my ($sel, $config) = get_selenium();
+my $test_bug_1 = $config->{test_bug_1};
+
+# When being logged out, the 'Commit' button should not be displayed.
+
+$sel->open_ok("/$config->{bugzilla_installation}/index.cgi?logout=1", undef, "Log out (if required)");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Logged Out");
+go_to_bug($sel, $test_bug_1);
+ok(!$sel->is_element_present('commit'), "Button 'Commit' not available");
+
+# Now 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");
+$sel->type_ok("short_desc", "Greetings from a powerless user");
+$sel->type_ok("comment", "File a bug with an empty CC list");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+my $bug1_id = $sel->get_value("//input[\@name='id' and \@type='hidden']");
+$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created");
+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("I need an email address 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/qa/t/test_votes.t b/qa/t/test_votes.t
new file mode 100644
index 000000000..99b191c80
--- /dev/null
+++ b/qa/t/test_votes.t
@@ -0,0 +1,251 @@
+# 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 strict;
+use warnings;
+use lib qw(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->select_ok("default_op_sys_id", "Unspecified");
+$sel->select_ok("default_platform_id", "Unspecified");
+$sel->click_ok('//input[@type="submit" and @value="Add"]');
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Product Created");
+$sel->click_ok("link=add at least one component");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Add component to the Eureka product");
+$sel->type_ok("component", "Pegasus");
+$sel->type_ok("description", "A constellation in the north hemisphere.");
+$sel->type_ok("initialowner", $config->{permanent_user}, "Setting the default owner");
+$sel->uncheck_ok("watch_user_auto");
+$sel->type_ok("watch_user", "pegasus\@eureka.bugs");
+$sel->check_ok("watch_user_auto");
+$sel->click_ok("create");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->title_is("Component Created");
+my $text = trim($sel->get_text("message"));
+ok($text =~ qr/The component Pegasus has been created/, "Component 'Pegasus' 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");
+$sel->type_ok("short_desc", "Aries");
+$sel->type_ok("comment", "1st constellation");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database');
+my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# 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");
+$sel->type_ok("short_desc", "Taurus");
+$sel->type_ok("comment", "2nd constellation");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database');
+my $bug2_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# 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");
+$sel->type_ok("short_desc", "Gemini");
+$sel->type_ok("comment", "3rd constellation");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database');
+my $bug3_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# 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");
+$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('//input[@type="submit" and @value="Save Changes"]');
+$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('//input[@type="submit" and @value="Save Changes"]');
+$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('//input[@type="submit" and @value="Save Changes"]');
+$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('//input[@type="submit" and @value="Save Changes"]');
+$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");
+$sel->type_ok("short_desc", "Cancer");
+$sel->type_ok("comment", "4th constellation");
+$sel->click_ok("commit");
+$sel->wait_for_page_to_load_ok(WAIT_TIME);
+$sel->is_text_present_ok('has been added to the database');
+my $bug4_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+
+# 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/qa/t/webservice_bug_add_attachment.t b/qa/t/webservice_bug_add_attachment.t
new file mode 100644
index 000000000..1ea0ca76a
--- /dev/null
+++ b/qa/t/webservice_bug_add_attachment.t
@@ -0,0 +1,227 @@
+# 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 strict;
+use warnings;
+use lib qw(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 = [ keys %{ $call->result->{attachments} } ];
+ $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/qa/t/webservice_bug_add_comment.t b/qa/t/webservice_bug_add_comment.t
new file mode 100644
index 000000000..64c410f3f
--- /dev/null
+++ b/qa/t/webservice_bug_add_comment.t
@@ -0,0 +1,168 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_bug_attachments.t b/qa/t/webservice_bug_attachments.t
new file mode 100644
index 000000000..da63ddd27
--- /dev/null
+++ b/qa/t/webservice_bug_attachments.t
@@ -0,0 +1,151 @@
+# 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 strict;
+use warnings;
+use lib qw(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/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/qa/t/webservice_bug_comments.t b/qa/t/webservice_bug_comments.t
new file mode 100644
index 000000000..df583c8ce
--- /dev/null
+++ b/qa/t/webservice_bug_comments.t
@@ -0,0 +1,175 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_bug_create.t b/qa/t/webservice_bug_create.t
new file mode 100644
index 000000000..640a714be
--- /dev/null
+++ b/qa/t/webservice_bug_create.t
@@ -0,0 +1,239 @@
+# 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 strict;
+use warnings;
+use lib qw(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 20 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/qa/t/webservice_bug_fields.t b/qa/t/webservice_bug_fields.t
new file mode 100644
index 000000000..d72b25170
--- /dev/null
+++ b/qa/t/webservice_bug_fields.t
@@ -0,0 +1,219 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_bug_get.t b/qa/t/webservice_bug_get.t
new file mode 100644
index 000000000..a68f92899
--- /dev/null
+++ b/qa/t/webservice_bug_get.t
@@ -0,0 +1,146 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+use Data::Dumper;
+use DateTime;
+use QA::Util;
+use QA::Tests qw(bug_tests PRIVATE_BUG_USER);
+use Test::More tests => 1012;
+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/qa/t/webservice_bug_get_bugs.t b/qa/t/webservice_bug_get_bugs.t
new file mode 100644
index 000000000..d83861350
--- /dev/null
+++ b/qa/t/webservice_bug_get_bugs.t
@@ -0,0 +1,146 @@
+# 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_bugs() #
+###########################################
+
+use strict;
+use warnings;
+use lib qw(lib);
+use Data::Dumper;
+use DateTime;
+use QA::Util;
+use QA::Tests qw(bug_tests PRIVATE_BUG_USER);
+use Test::More tests => 1012;
+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"] },
+ 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"];
+$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} = [];
+$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}
+ && exists $bug->{deadline},
+ '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}
+ && !exists $bug->{deadline},
+ '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/qa/t/webservice_bug_history.t b/qa/t/webservice_bug_history.t
new file mode 100644
index 000000000..3153834fa
--- /dev/null
+++ b/qa/t/webservice_bug_history.t
@@ -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.
+
+#########################################
+# Test for xmlrpc call to Bug.history() #
+#########################################
+
+use strict;
+use warnings;
+use lib qw(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/qa/t/webservice_bug_legal_values.t b/qa/t/webservice_bug_legal_values.t
new file mode 100644
index 000000000..8e1b039a3
--- /dev/null
+++ b/qa/t/webservice_bug_legal_values.t
@@ -0,0 +1,100 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_bug_search.t b/qa/t/webservice_bug_search.t
new file mode 100644
index 000000000..0a4dcd666
--- /dev/null
+++ b/qa/t/webservice_bug_search.t
@@ -0,0 +1,185 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER);
+use DateTime;
+use List::MoreUtils qw(uniq);
+use Test::More;
+
+my ($config, @clients) = get_rpc_clients();
+plan tests => $config->{test_extensions} ? 515 : 506;
+
+my ($public_bug, $private_bug) = $clients[0]->bz_create_test_bugs('private');
+
+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/qa/t/webservice_bug_update.t b/qa/t/webservice_bug_update.t
new file mode 100644
index 000000000..a9a01386a
--- /dev/null
+++ b/qa/t/webservice_bug_update.t
@@ -0,0 +1,683 @@
+# 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 strict;
+use warnings;
+use lib qw(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 => 927;
+
+use constant NONEXISTANT_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});
+
+ 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' },
+ # FIXME: Also check strict_isolation at some point in the future, perhaps.
+ ],
+
+ blocks => [
+ { value => { add => [NONEXISTANT_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 => NONEXISTANT_BUG,
+ error => 'does not exist',
+ test => 'Cannot dup to a nonexistant 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 => 'The legal keyword names are listed here',
+ test => 'adding invalid keyword fails' },
+ { value => { remove => [random_string(20)] },
+ error => 'The legal keyword names are listed here',
+ test => 'removing invalid keyword fails' },
+ { value => { set => [random_string(20)] },
+ error => 'The legal keyword names are listed here',
+ 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/qa/t/webservice_bug_update_see_also.t b/qa/t/webservice_bug_update_see_also.t
new file mode 100644
index 000000000..3bdc3dc25
--- /dev/null
+++ b/qa/t/webservice_bug_update_see_also.t
@@ -0,0 +1,82 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_bugzilla.t b/qa/t/webservice_bugzilla.t
new file mode 100644
index 000000000..fef57cca1
--- /dev/null
+++ b/qa/t/webservice_bugzilla.t
@@ -0,0 +1,45 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_group_create.t b/qa/t/webservice_group_create.t
new file mode 100644
index 000000000..346800b0e
--- /dev/null
+++ b/qa/t/webservice_group_create.t
@@ -0,0 +1,97 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_jsonp.t b/qa/t/webservice_jsonp.t
new file mode 100644
index 000000000..dd62b6392
--- /dev/null
+++ b/qa/t/webservice_jsonp.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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_product.t b/qa/t/webservice_product.t
new file mode 100644
index 000000000..7f993899f
--- /dev/null
+++ b/qa/t/webservice_product.t
@@ -0,0 +1,109 @@
+# 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_products() #
+########################################
+
+use strict;
+use warnings;
+use lib qw(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/qa/t/webservice_product_create.t b/qa/t/webservice_product_create.t
new file mode 100644
index 000000000..05b82d60c
--- /dev/null
+++ b/qa/t/webservice_product_create.t
@@ -0,0 +1,163 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_product_get.t b/qa/t/webservice_product_get.t
new file mode 100644
index 000000000..0128f3348
--- /dev/null
+++ b/qa/t/webservice_product_get.t
@@ -0,0 +1,109 @@
+# 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 strict;
+use warnings;
+use lib qw(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/qa/t/webservice_user_create.t b/qa/t/webservice_user_create.t
new file mode 100644
index 000000000..20301a6c5
--- /dev/null
+++ b/qa/t/webservice_user_create.t
@@ -0,0 +1,108 @@
+# 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 strict;
+use warnings;
+use lib qw(lib);
+use QA::Util;
+use Test::More tests => 69;
+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 => $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/qa/t/webservice_user_get.t b/qa/t/webservice_user_get.t
new file mode 100644
index 000000000..7d50bf22e
--- /dev/null
+++ b/qa/t/webservice_user_get.t
@@ -0,0 +1,218 @@
+# 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 strict;
+use warnings;
+use lib qw(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 => ['Master', 'editbugs'] },
+ test => 'Limiting the return value to some groups you do not belong to fails',
+ error => 'The group you specified, Master, 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},
+ 'Users can get the list of saved searches and reports for their own account');
+ }
+ else {
+ ok(!exists $item->{saved_searches},
+ "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/qa/t/webservice_user_login_logout.t b/qa/t/webservice_user_login_logout.t
new file mode 100644
index 000000000..2affb961c
--- /dev/null
+++ b/qa/t/webservice_user_login_logout.t
@@ -0,0 +1,124 @@
+# 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 strict;
+use warnings;
+use lib qw(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 username 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/qa/t/webservice_user_offer_account_by_email.t b/qa/t/webservice_user_offer_account_by_email.t
new file mode 100644
index 000000000..ae311afb8
--- /dev/null
+++ b/qa/t/webservice_user_offer_account_by_email.t
@@ -0,0 +1,59 @@
+# 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 strict;
+use warnings;
+use lib qw(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');
+}