summaryrefslogtreecommitdiffstats
path: root/xt/lib/QA
diff options
context:
space:
mode:
authorDavid Lawrence <dkl@mozilla.com>2016-02-26 18:57:55 +0100
committerDavid Lawrence <dkl@mozilla.com>2016-02-26 18:57:55 +0100
commit9b6ec1f545da1cc4088ddf9cc117747954e58e65 (patch)
tree6cc3eb342a740b795052e587756f6c33438b772a /xt/lib/QA
parent6f70920f2d2bb038a371e3cb3debff44f7001fa8 (diff)
downloadbugzilla-9b6ec1f545da1cc4088ddf9cc117747954e58e65.tar.gz
bugzilla-9b6ec1f545da1cc4088ddf9cc117747954e58e65.tar.xz
Bug 1069799 - move the QA repository into the main repository
r=LpSolit
Diffstat (limited to 'xt/lib/QA')
-rw-r--r--xt/lib/QA/REST.pm65
-rw-r--r--xt/lib/QA/RPC.pm289
-rw-r--r--xt/lib/QA/RPC/JSONRPC.pm174
-rw-r--r--xt/lib/QA/RPC/XMLRPC.pm26
-rw-r--r--xt/lib/QA/Tests.pm115
-rw-r--r--xt/lib/QA/Util.pm372
6 files changed, 1041 insertions, 0 deletions
diff --git a/xt/lib/QA/REST.pm b/xt/lib/QA/REST.pm
new file mode 100644
index 000000000..4de985668
--- /dev/null
+++ b/xt/lib/QA/REST.pm
@@ -0,0 +1,65 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package QA::REST;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use autodie;
+
+use LWP::UserAgent;
+use JSON;
+use QA::Util;
+
+use parent qw(LWP::UserAgent Exporter);
+
+@QA::REST::EXPORT = qw(
+ MUST_FAIL
+ get_rest_client
+);
+
+use constant MUST_FAIL => 1;
+
+sub get_rest_client {
+ my $rest_client = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } );
+ bless($rest_client, 'QA::REST');
+ my $config = $rest_client->{bz_config} = get_config();
+ $rest_client->{bz_url} = $config->{browser_url} . '/' . $config->{bugzilla_installation} . '/rest/';
+ $rest_client->{bz_default_headers} = {'Accept' => 'application/json', 'Content-Type' => 'application/json'};
+ return $rest_client;
+}
+
+sub bz_config { return $_[0]->{bz_config}; }
+
+sub call {
+ my ($self, $method, $data, $http_verb, $expect_to_fail) = @_;
+ $http_verb = lc($http_verb || 'GET');
+ $data //= {};
+
+ my %args = %{ $self->{bz_default_headers} };
+ # We do not pass the API key in the URL, so that it's not logged by the web server.
+ if ($http_verb eq 'get' && $data->{api_key}) {
+ $args{'X-BUGZILLA-API-KEY'} = $data->{api_key};
+ }
+ elsif ($http_verb ne 'get') {
+ $args{Content} = encode_json($data);
+ }
+
+ my $response = $self->$http_verb($self->{bz_url} . $method, %args);
+ my $res = decode_json($response->decoded_content);
+ if ($response->is_success xor $expect_to_fail) {
+ return $res;
+ }
+ else {
+ die 'error ' . $res->{code} . ': ' . $res->{message} . "\n";
+ }
+}
diff --git a/xt/lib/QA/RPC.pm b/xt/lib/QA/RPC.pm
new file mode 100644
index 000000000..63a7d9503
--- /dev/null
+++ b/xt/lib/QA/RPC.pm
@@ -0,0 +1,289 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::RPC;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use Data::Dumper;
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER create_bug_fields);
+use Storable qw(dclone);
+use Test::More;
+
+sub bz_config {
+ my $self = shift;
+ $self->{bz_config} ||= QA::Util::get_config();
+ return $self->{bz_config};
+}
+
+# True if we're doing calls over GET instead of POST.
+sub bz_get_mode { return 0 }
+
+# When doing bz_log_in over GET, we can't actually call User.login,
+# we just store credentials here and then pass them as Bugzilla_login
+# and Bugzilla_password with every future call until User.logout is called
+# (which actually just calls _bz_clear_credentials, under GET).
+sub _bz_credentials {
+ my ($self, $user, $pass) = @_;
+ if (@_ == 3) {
+ $self->{_bz_credentials}->{user} = $user;
+ $self->{_bz_credentials}->{pass} = $pass;
+ }
+ return $self->{_bz_credentials};
+}
+sub _bz_clear_credentials { delete $_[0]->{_bz_credentials} }
+
+################################
+# Helpers for RPC test scripts #
+################################
+
+sub bz_log_in {
+ my ($self, $user) = @_;
+ my $username = $self->bz_config->{"${user}_user_login"};
+ my $password = $self->bz_config->{"${user}_user_passwd"};
+
+ if ($self->bz_get_mode) {
+ $self->_bz_credentials($username, $password);
+ return;
+ }
+
+ my $call = $self->bz_call_success(
+ 'User.login', { login => $username, password => $password });
+ cmp_ok($call->result->{id}, 'gt', 0, $self->TYPE . ": Logged in as $user");
+ $self->{_bz_credentials}->{token} = $call->result->{token};
+}
+
+sub bz_call_success {
+ my ($self, $method, $orig_args, $test_name) = @_;
+ my $args = $orig_args ? dclone($orig_args) : {};
+
+ if ($self->bz_get_mode and $method eq 'User.logout') {
+ $self->_bz_clear_credentials();
+ return;
+ }
+
+ my $call;
+ # Under XMLRPC::Lite, if we pass undef as the second argument,
+ # it sends a single param <value />, which shows up as an
+ # empty string on the Bugzilla side.
+ if ($self->{_bz_credentials}->{token}) {
+ $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
+ }
+
+ if (scalar keys %$args) {
+ $call = $self->call($method, $args);
+ }
+ else {
+ $call = $self->call($method);
+ }
+ $test_name ||= "$method returned successfully";
+ $self->_handle_undef_response($test_name) if !$call;
+ ok(!$call->fault, $self->TYPE . ": $test_name")
+ or diag($call->faultstring);
+
+ if ($method eq 'User.logout') {
+ delete $self->{_bz_credentials}->{token};
+ }
+ return $call;
+}
+
+sub bz_call_fail {
+ my ($self, $method, $orig_args, $faultstring, $test_name) = @_;
+ my $args = $orig_args ? dclone($orig_args) : {};
+
+ if ($self->{_bz_credentials}->{token}) {
+ $args->{Bugzilla_token} = $self->{_bz_credentials}->{token};
+ }
+
+ $test_name ||= "$method failed (as intended)";
+ my $call = $self->call($method, $args);
+ $self->_handle_undef_response($test_name) if !$call;
+ ok($call->fault, $self->TYPE . ": $test_name")
+ or diag("Returned: " . Dumper($call->result));
+ if (defined $faultstring) {
+ cmp_ok(trim($call->faultstring), '=~', $faultstring,
+ $self->TYPE . ": Got correct fault for $method");
+ }
+ ok($call->faultcode
+ && (($call->faultcode < 32000 && $call->faultcode > -32000)
+ # Fault codes 32610 and above are OK because they are errors
+ # that we expect and test for sometimes.
+ || $call->faultcode >= 32610),
+ $self->TYPE . ': Fault code is set properly')
+ or diag("Code: " . $call->faultcode
+ . " Message: " . $call->faultstring);
+
+ return $call;
+}
+
+sub _handle_undef_response {
+ my ($self, $test_name) = @_;
+ my $response = $self->transport->http_response;
+ die "$test_name:\n", $response->as_string;
+}
+
+sub bz_get_products {
+ my ($self) = @_;
+ $self->bz_log_in('QA_Selenium_TEST');
+
+ my $accessible = $self->bz_call_success('Product.get_accessible_products');
+ my $prod_call = $self->bz_call_success('Product.get', $accessible->result);
+ my %products;
+ foreach my $prod (@{ $prod_call->result->{products} }) {
+ $products{$prod->{name}} = $prod->{id};
+ }
+
+ $self->bz_call_success('User.logout');
+ return \%products;
+}
+
+sub _string_array { map { random_string() } (1..$_[0]) }
+
+sub bz_create_test_bugs {
+ my ($self, $second_private) = @_;
+ my $config = $self->bz_config;
+
+ my @whiteboard_strings = _string_array(3);
+ my @summary_strings = _string_array(3);
+
+ my $public_bug = create_bug_fields($config);
+ $public_bug->{whiteboard} = join(' ', @whiteboard_strings);
+ $public_bug->{summary} = join(' ', @summary_strings);
+
+ my $private_bug = dclone($public_bug);
+ if ($second_private) {
+ $private_bug->{product} = 'QA-Selenium-TEST';
+ $private_bug->{component} = 'QA-Selenium-TEST';
+ $private_bug->{target_milestone} = 'QAMilestone';
+ $private_bug->{version} = 'QAVersion';
+ # Although we don't directly use this, this helps some tests that
+ # depend on the values in $private_bug.
+ $private_bug->{creator} = $config->{PRIVATE_BUG_USER . '_user_login'};
+ }
+
+ my @create_bugs = (
+ { user => 'editbugs',
+ args => $public_bug,
+ test => 'Create a public bug' },
+ { user => $second_private ? PRIVATE_BUG_USER : 'editbugs',
+ args => $private_bug,
+ test => $second_private ? 'Create a private bug'
+ : 'Create a second public bug' },
+ );
+
+ my $post_success = sub {
+ my ($call, $t) = @_;
+ my $id = $call->result->{id};
+ $t->{args}->{id} = $id;
+ };
+
+ # Creating the bugs isn't really a test, it's just preliminary work
+ # for the tests. So we just run it with one of the RPC clients.
+ $self->bz_run_tests(tests => \@create_bugs, method => 'Bug.create',
+ post_success => $post_success);
+
+ return ($public_bug, $private_bug);
+}
+
+sub bz_run_tests {
+ my ($self, %params) = @_;
+ # Required params
+ my $config = $self->bz_config;
+ my $tests = $params{tests};
+ my $method = $params{method};
+
+ # Optional params
+ my $post_success = $params{post_success};
+ my $pre_call = $params{pre_call};
+
+ my $former_user = '';
+ foreach my $t (@$tests) {
+ # Only logout/login if the user has changed since the last test
+ # (this saves us LOTS of needless logins).
+ my $user = $t->{user} || '';
+ if ($former_user ne $user) {
+ $self->bz_call_success('User.logout') if $former_user;
+ $self->bz_log_in($user) if $user;
+ $former_user = $user;
+ }
+
+ $pre_call->($t, $self) if $pre_call;
+
+ if ($t->{error}) {
+ $self->bz_call_fail($method, $t->{args}, $t->{error}, $t->{test});
+ }
+ else {
+ my $call = $self->bz_call_success($method, $t->{args}, $t->{test});
+ if ($call->result && $post_success) {
+ $post_success->($call, $t, $self);
+ }
+ }
+ }
+
+ $self->bz_call_success('User.logout') if $former_user;
+}
+
+sub bz_test_bug {
+ my ($self, $fields, $bug, $expect, $t, $creation_time) = @_;
+
+ foreach my $field (sort @$fields) {
+ # "description" is used by Bug.create but comments are not returned
+ # by Bug.get or Bug.search.
+ next if $field eq 'description';
+
+ my @include = @{ $t->{args}->{include_fields} || [] };
+ my @exclude = @{ $t->{args}->{exclude_fields} || [] };
+ if ( (@include and !grep($_ eq $field, @include))
+ or (@exclude and grep($_ eq $field, @exclude)) )
+ {
+ ok(!exists $bug->{$field}, "$field is not included")
+ or diag Dumper($bug);
+ next;
+ }
+
+ if ($field =~ /^is_/) {
+ ok(defined $bug->{$field}, $self->TYPE . ": $field is not null");
+ is($bug->{$field} ? 1 : 0, $expect->{$field} ? 1 : 0,
+ $self->TYPE . ": $field has the right boolean value");
+ }
+ elsif ($field eq 'cc') {
+ foreach my $cc_item (@{ $expect->{cc} || [] }) {
+ ok(grep($_ eq $cc_item, @{ $bug->{cc} }),
+ $self->TYPE . ": $field contains $cc_item");
+ }
+ }
+ elsif ($field eq 'creation_time' or $field eq 'last_change_time') {
+ my $creation_day;
+ # XML-RPC and JSON-RPC have different date formats.
+ if ($self->isa('QA::RPC::XMLRPC')) {
+ $creation_day = $creation_time->ymd('');
+ }
+ else {
+ $creation_day = $creation_time->ymd;
+ }
+
+ like($bug->{$field}, qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
+ $self->TYPE . ": $field has the right format");
+ }
+ else {
+ is_deeply($bug->{$field}, $expect->{$field},
+ $self->TYPE . ": $field value is correct");
+ }
+ }
+}
+
+1;
+
+__END__
diff --git a/xt/lib/QA/RPC/JSONRPC.pm b/xt/lib/QA/RPC/JSONRPC.pm
new file mode 100644
index 000000000..7a085e026
--- /dev/null
+++ b/xt/lib/QA/RPC/JSONRPC.pm
@@ -0,0 +1,174 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::RPC::JSONRPC;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../../lib", "$RealBin/../../../../local/lib/perl5";
+
+use QA::RPC;
+BEGIN {
+ our @ISA = qw(QA::RPC);
+
+ if (eval { require JSON::RPC::Client }) {
+ push(@ISA, 'JSON::RPC::Client');
+ }
+ else {
+ require JSON::RPC::Legacy::Client;
+ push(@ISA, 'JSON::RPC::Legacy::Client');
+ }
+}
+
+use URI::Escape;
+
+use constant DATETIME_REGEX => qr/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/;
+sub TYPE {
+ my ($self) = @_;
+ return $self->bz_get_mode ? 'JSON-RPC GET' : 'JSON-RPC';
+}
+
+#################################
+# Consistency with XMLRPC::Lite #
+#################################
+
+sub ua {
+ my $self = shift;
+ if ($self->{ua} and not $self->{ua}->isa('QA::RPC::UserAgent')) {
+ bless $self->{ua}, 'QA::RPC::UserAgent';
+ }
+ return $self->SUPER::ua(@_);
+}
+sub transport { $_[0]->ua }
+
+sub bz_get_mode {
+ my ($self, $value) = @_;
+ $self->{bz_get_mode} = $value if @_ > 1;
+ return $self->{bz_get_mode};
+}
+
+sub _bz_callback {
+ my ($self, $value) = @_;
+ $self->{bz_callback} = $value if @_ > 1;
+ return $self->{bz_callback};
+}
+
+sub call {
+ my $self = shift;
+ my ($method, $args) = @_;
+ my %params = ( method => $method );
+ $params{params} = $args ? [$args] : [];
+
+ my $config = $self->bz_config;
+ my $url = $config->{browser_url} . "/"
+ . $config->{bugzilla_installation} . "/jsonrpc.cgi";
+ my $result;
+ if ($self->bz_get_mode) {
+ my $method_escaped = uri_escape($method);
+ $url .= "?method=$method_escaped";
+ if (my $cred = $self->_bz_credentials) {
+ $args->{Bugzilla_login} = $cred->{user}
+ if !exists $args->{Bugzilla_login};
+ $args->{Bugzilla_password} = $cred->{pass}
+ if !exists $args->{Bugzilla_password};
+ }
+ if ($args) {
+ my $params_json = $self->json->encode($args);
+ my $params_escaped = uri_escape($params_json);
+ $url .= "&params=$params_escaped";
+ }
+ if ($self->version eq '1.1') {
+ $url .= "&version=1.1";
+ }
+ my $callback = delete $args->{callback};
+ if (defined $callback) {
+ $self->_bz_callback($callback);
+ $url .= "&callback=" . uri_escape($callback);
+ }
+ $result = $self->SUPER::call($url);
+ }
+ else {
+ $result = $self->SUPER::call($url, \%params);
+ }
+
+ if ($result) {
+ bless $result, 'QA::RPC::JSONRPC::ReturnObject';
+ }
+ return $result;
+}
+
+sub _get {
+ my $self = shift;
+ my $result = $self->SUPER::_get(@_);
+ # Simple JSONP support for tests. We just remove the callback from
+ # the return value.
+ my $callback = $self->_bz_callback;
+ if (defined $callback and $result->is_success) {
+ my $content = $result->content;
+ $content =~ s/^(?:\/\*\*\/)?\Q$callback(\E(.*)\)$/$1/s;
+ $result->content($content);
+ # We don't need this anymore, and we don't want it to affect
+ # future calls.
+ delete $self->{bz_callback};
+ }
+ return $result;
+}
+
+1;
+
+package QA::RPC::JSONRPC::ReturnObject;
+use strict;
+
+BEGIN {
+ if (eval { require JSON::RPC::Client }) {
+ our @ISA = qw(JSON::RPC::ReturnObject);
+ }
+ else {
+ require JSON::RPC::Legacy::Client;
+ our @ISA = qw(JSON::RPC::Legacy::ReturnObject);
+ }
+}
+
+#################################
+# Consistency with XMLRPC::Lite #
+#################################
+
+sub faultstring { $_[0]->{content}->{error}->{message} }
+sub faultcode { $_[0]->{content}->{error}->{code} }
+sub fault { $_[0]->is_error }
+
+1;
+
+package QA::RPC::UserAgent;
+use strict;
+use base qw(LWP::UserAgent);
+
+########################################
+# Consistency with XMLRPC::Lite's ->ua #
+########################################
+
+sub send_request {
+ my $self = shift;
+ my $response = $self->SUPER::send_request(@_);
+ $self->http_response($response);
+ # JSON::RPC::Client can't handle 500 responses, even though
+ # they're required by the JSON-RPC spec.
+ $response->code(200);
+ return $response;
+}
+
+# Copied directly from SOAP::Lite::Transport::HTTP.
+sub http_response {
+ my $self = shift;
+ if (@_) { $self->{'_http_response'} = shift; return $self }
+ return $self->{'_http_response'};
+}
diff --git a/xt/lib/QA/RPC/XMLRPC.pm b/xt/lib/QA/RPC/XMLRPC.pm
new file mode 100644
index 000000000..cb227fa9c
--- /dev/null
+++ b/xt/lib/QA/RPC/XMLRPC.pm
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::RPC::XMLRPC;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../../lib", "$RealBin/../../../../local/lib/perl5";
+
+use base qw(QA::RPC XMLRPC::Lite);
+
+use constant TYPE => 'XML-RPC';
+use constant DATETIME_REGEX => qr/^\d{8}T\d\d:\d\d:\d\d$/;
+
+1;
+
+__END__
diff --git a/xt/lib/QA/Tests.pm b/xt/lib/QA/Tests.pm
new file mode 100644
index 000000000..fe5f2d067
--- /dev/null
+++ b/xt/lib/QA/Tests.pm
@@ -0,0 +1,115 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::Tests;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use base qw(Exporter);
+our @EXPORT_OK = qw(
+ PRIVATE_BUG_USER
+ STANDARD_BUG_TESTS
+ bug_tests
+ create_bug_fields
+);
+
+use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ALIAS => 'aaaaaaa12345';
+use constant PRIVATE_BUG_USER => 'QA_Selenium_TEST';
+
+use constant CREATE_BUG => {
+ 'priority' => 'Highest',
+ 'status' => 'CONFIRMED',
+ 'version' => 'unspecified',
+ 'creator' => 'editbugs',
+ 'description' => '-- Comment Created By Bugzilla XML-RPC Tests --',
+ 'cc' => ['unprivileged'],
+ 'component' => 'c1',
+ 'platform' => 'PC',
+ # It's necessary to assign the bug to somebody who isn't in the
+ # timetracking group, for the Bug.update tests.
+ 'assigned_to' => PRIVATE_BUG_USER,
+ 'summary' => 'WebService Test Bug',
+ 'product' => 'Another Product',
+ 'op_sys' => 'Linux',
+ 'severity' => 'normal',
+ 'qa_contact' => 'canconfirm',
+ version => 'Another1',
+ url => 'http://www.bugzilla.org/',
+ target_milestone => 'AnotherMS1',
+};
+
+sub create_bug_fields {
+ my ($config) = @_;
+ my %bug = %{ CREATE_BUG() };
+ foreach my $field (qw(creator assigned_to qa_contact)) {
+ my $value = $bug{$field};
+ $bug{$field} = $config->{"${value}_user_login"};
+ }
+ $bug{cc} = [map { $config->{$_ . "_user_login"} } @{ $bug{cc} }];
+ return \%bug;
+}
+
+sub bug_tests {
+ my ($public_id, $private_id) = @_;
+ return [
+ { args => { ids => [$private_id] },
+ error => "You are not authorized to access",
+ test => 'Logged-out user cannot access a private bug',
+ },
+ { args => { ids => [$public_id] },
+ test => 'Logged-out user can access a public bug.',
+ },
+ { args => { ids => [INVALID_BUG_ID] },
+ error => "not a valid bug number",
+ test => 'Passing invalid bug id returns error "Invalid Bug ID"',
+ },
+ { args => { ids => [undef] },
+ error => "You must enter a valid bug number",
+ test => 'Passing undef as bug id param returns error "Invalid Bug ID"',
+ },
+ { args => { ids => [INVALID_BUG_ALIAS] },
+ error => "nor an alias to a bug",
+ test => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+ },
+
+ { user => 'editbugs',
+ args => { ids => [$private_id] },
+ error => "You are not authorized to access",
+ test => 'Access to a private bug is denied to a user without privs',
+ },
+ { user => 'unprivileged',
+ args => { ids => [$public_id] },
+ test => 'User without privs can access a public bug',
+ },
+ { user => 'admin',
+ args => { ids => [$public_id] },
+ test => 'Admin can access a public bug.',
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { ids => [$private_id] },
+ test => 'User with privs can successfully access a private bug',
+ },
+ # This helps webservice_bug_attachment get private attachment ids
+ # from the public bug, and doesn't hurt for the other tests.
+ { user => PRIVATE_BUG_USER,
+ args => { ids => [$public_id] },
+ test => 'User with privs can also access the public bug',
+ },
+ ];
+}
+
+use constant STANDARD_BUG_TESTS => bug_tests('public_bug', 'private_bug');
+
+1;
diff --git a/xt/lib/QA/Util.pm b/xt/lib/QA/Util.pm
new file mode 100644
index 000000000..e122e41db
--- /dev/null
+++ b/xt/lib/QA/Util.pm
@@ -0,0 +1,372 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+
+package QA::Util;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5";
+
+use autodie;
+use Data::Dumper;
+use Test::More;
+
+use parent qw(Exporter);
+@QA::Util::EXPORT = qw(
+ trim
+ url_quote
+ random_string
+
+ log_in
+ logout
+ file_bug_in_product
+ create_bug
+ edit_bug
+ edit_bug_and_return
+ go_to_bug
+ go_to_home
+ go_to_admin
+ edit_product
+ add_product
+ open_advanced_search_page
+ set_parameters
+
+ get_selenium
+ get_rpc_clients
+ get_config
+
+ WAIT_TIME
+ CHROME_MODE
+);
+
+# How long we wait for pages to load.
+use constant WAIT_TIME => 60000;
+use constant CONF_FILE => "$RealBin/../config/selenium_test.conf";
+use constant CHROME_MODE => 1;
+use constant NDASH => chr(0x2013);
+
+#####################
+# Utility Functions #
+#####################
+
+sub random_string {
+ my $size = shift || 30; # default to 30 chars if nothing specified
+ return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size));
+}
+
+# Remove consecutive as well as leading and trailing whitespaces.
+sub trim {
+ my ($str) = @_;
+ if ($str) {
+ $str =~ s/[\r\n\t\s]+/ /g;
+ $str =~ s/^\s+//g;
+ $str =~ s/\s+$//g;
+ }
+ return $str;
+}
+
+# This originally came from CGI.pm, by Lincoln D. Stein
+sub url_quote {
+ my ($toencode) = (@_);
+ $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
+ return $toencode;
+}
+
+###################
+# Setup Functions #
+###################
+
+sub get_config {
+ # read the test configuration file
+ my $conf_file = CONF_FILE;
+ my $config = do($conf_file)
+ or die "can't read configuration '$conf_file': $!$@";
+ return $config;
+}
+
+sub get_selenium {
+ my $chrome_mode = shift;
+ my $config = get_config();
+
+ require Test::WWW::Selenium;
+ require WWW::Selenium::Util;
+
+ if (!WWW::Selenium::Util::server_is_running()) {
+ die "Selenium Server isn't running!";
+ }
+
+ my $sel = Test::WWW::Selenium->new(
+ host => $config->{host},
+ port => $config->{port},
+ browser => $chrome_mode ? $config->{experimental_browser_launcher} : $config->{browser},
+ browser_url => $config->{browser_url}
+ );
+
+ return ($sel, $config);
+}
+
+sub get_xmlrpc_client {
+ my $config = get_config();
+ my $xmlrpc_url = $config->{browser_url} . "/" .
+ $config->{bugzilla_installation} . "/xmlrpc.cgi";
+
+ require QA::RPC::XMLRPC;
+ my $rpc = QA::RPC::XMLRPC->new(proxy => $xmlrpc_url);
+ return ($rpc, $config);
+}
+
+sub get_jsonrpc_client {
+ my ($get_mode) = @_;
+ require QA::RPC::JSONRPC;
+ my $rpc = QA::RPC::JSONRPC->new();
+ # If we don't set a long timeout, then the Bug.add_comment test
+ # where we add a too-large comment fails.
+ $rpc->transport->timeout(180);
+ $rpc->version($get_mode ? '1.1' : '1.0');
+ $rpc->bz_get_mode($get_mode);
+ return $rpc;
+}
+
+sub get_rpc_clients {
+ my ($xmlrpc, $config) = get_xmlrpc_client();
+ my $jsonrpc = get_jsonrpc_client();
+ my $jsonrpc_get = get_jsonrpc_client('GET');
+ return ($config, $xmlrpc, $jsonrpc, $jsonrpc_get);
+}
+
+################################
+# Helpers for Selenium Scripts #
+################################
+
+sub go_to_home {
+ my ($sel, $config) = @_;
+ $sel->open_ok("/$config->{bugzilla_installation}/", undef, "Go to the home page");
+ $sel->title_is("Bugzilla Main Page");
+}
+
+# Go to the home/login page and log in.
+sub log_in {
+ my ($sel, $config, $user) = @_;
+
+ go_to_home($sel, $config);
+ $sel->type_ok("Bugzilla_login_top", $config->{"${user}_user_login"}, "Enter $user login name");
+ $sel->type_ok("Bugzilla_password_top", $config->{"${user}_user_passwd"}, "Enter $user password");
+ $sel->click_ok("log_in_top", undef, "Submit credentials");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Bugzilla Main Page", "User is logged in");
+}
+
+# Log out. Will fail if you are not logged in.
+sub logout {
+ my $sel = shift;
+
+ $sel->click_ok("link=Log out", undef, "Logout");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Logged Out");
+}
+
+# Display the bug form to enter a bug in the given product.
+sub file_bug_in_product {
+ my ($sel, $product, $classification) = @_;
+
+ $classification ||= "Unclassified";
+ $sel->click_ok("link=New", undef, "Go create a new bug");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ my $title = $sel->get_title();
+ if ($sel->is_text_present("Select Classification")) {
+ ok(1, "More than one enterable classification available. Display them in a list");
+ $sel->click_ok("link=$classification", undef, "Choose $classification");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $title = $sel->get_title();
+ }
+ if ($title eq "Enter Bug") {
+ ok(1, "Display the list of enterable products");
+ $sel->click_ok("link=$product", undef, "Choose $product");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ }
+ else {
+ ok(1, "Only one product available in $classification. Skipping the 'Choose product' page.")
+ }
+ $sel->title_is("Enter Bug: $product", "Display form to enter bug data");
+}
+
+sub create_bug {
+ my ($sel, $bug_summary) = @_;
+ my $ndash = NDASH;
+
+ $sel->click_ok('commit');
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ my $bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]');
+ $sel->title_like(qr/$bug_id $ndash( \(.*\))? $bug_summary/, "Bug $bug_id created with summary '$bug_summary'");
+ return $bug_id;
+}
+
+sub edit_bug {
+ my ($sel, $bug_id, $bug_summary, $options) = @_;
+ my $ndash = NDASH;
+ my $btn_id = $options ? $options->{id} : 'commit';
+
+ $sel->click_ok($btn_id);
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("$bug_id $ndash $bug_summary", "Changes submitted to bug $bug_id");
+ # If the web browser doesn't support history.ReplaceState or has it turned off,
+ # "Bug FIXME processed" is displayed instead (as in Bugzilla 4.0 and older).
+ # $sel->title_is("Bug $bug_id processed", "Changes submitted to bug $bug_id");
+}
+
+sub edit_bug_and_return {
+ my ($sel, $bug_id, $bug_summary, $options) = @_;
+ my $ndash = NDASH;
+ edit_bug($sel, $bug_id, $bug_summary, $options);
+ $sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug_id')]");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("$bug_id $ndash $bug_summary", "Returning back to bug $bug_id");
+}
+
+# Go to show_bug.cgi.
+sub go_to_bug {
+ my ($sel, $bug_id) = @_;
+
+ $sel->type_ok("quicksearch_top", $bug_id);
+ $sel->click_ok("find_top", undef, "Go to bug $bug_id");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ my $bug_title = $sel->get_title();
+ utf8::encode($bug_title) if utf8::is_utf8($bug_title);
+ $sel->title_like(qr/^$bug_id /, $bug_title);
+}
+
+# Go to admin.cgi.
+sub go_to_admin {
+ my $sel = shift;
+
+ $sel->click_ok("link=Administration", undef, "Go to the Admin page");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_like(qr/^Administer your installation/, "Display admin.cgi");
+}
+
+# Go to editproducts.cgi and display the given product.
+sub edit_product {
+ my ($sel, $product, $classification) = @_;
+
+ $classification ||= "Unclassified";
+ go_to_admin($sel);
+ $sel->click_ok("link=Products", undef, "Go to the Products page");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ my $title = $sel->get_title();
+ if ($title eq "Select Classification") {
+ ok(1, "More than one enterable classification available. Display them in a list");
+ $sel->click_ok("link=$classification", undef, "Choose $classification");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ }
+ else {
+ $sel->title_is("Select product", "Display the list of enterable products");
+ }
+ $sel->click_ok("link=$product", undef, "Choose $product");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Edit Product '$product'", "Display properties of $product");
+}
+
+sub add_product {
+ my ($sel, $classification) = @_;
+
+ $classification ||= "Unclassified";
+ go_to_admin($sel);
+ $sel->click_ok("link=Products", undef, "Go to the Products page");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ my $title = $sel->get_title();
+ if ($title eq "Select Classification") {
+ ok(1, "More than one enterable classification available. Display them in a list");
+ $sel->click_ok("//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]",
+ undef, "Add product to $classification");
+ }
+ else {
+ $sel->title_is("Select product", "Display the list of enterable products");
+ $sel->click_ok("link=Add", undef, "Add a new product");
+ }
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Add Product", "Display the new product form");
+}
+
+sub open_advanced_search_page {
+ my $sel = shift;
+
+ $sel->click_ok("link=Search");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ my $title = $sel->get_title();
+ if ($title eq "Simple Search") {
+ ok(1, "Display the simple search form");
+ $sel->click_ok("link=Advanced Search");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ }
+ $sel->title_is("Search for bugs", "Display the Advanced search form");
+}
+
+# $params is a hashref of the form:
+# {section1 => { param1 => {type => '(text|select)', value => 'foo'},
+# param2 => {type => '(text|select)', value => 'bar'},
+# param3 => undef },
+# section2 => { param4 => ...},
+# }
+# section1, section2, ... is the name of the section
+# param1, param2, ... is the name of the parameter (which must belong to the given section)
+# type => 'text' is for text fields
+# type => 'select' is for drop-down select fields
+# undef is for radio buttons (in which case the parameter must be the ID of the radio button)
+# value => 'foo' is the value of the parameter (either text or label)
+sub set_parameters {
+ my ($sel, $params) = @_;
+
+ go_to_admin($sel);
+ $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page");
+ $sel->wait_for_page_to_load(WAIT_TIME);
+ $sel->title_is("Configuration: Required Settings");
+ my $last_section = "Required Settings";
+
+ foreach my $section (keys %$params) {
+ if ($section ne $last_section) {
+ $sel->click_ok("link=$section");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Configuration: $section");
+ $last_section = $section;
+ }
+ my $param_list = $params->{$section};
+ foreach my $param (keys %$param_list) {
+ my $data = $param_list->{$param};
+ if (defined $data) {
+ my $type = $data->{type};
+ my $value = $data->{value};
+
+ if ($type eq 'text') {
+ $sel->type_ok($param, $value);
+ }
+ elsif ($type eq 'select') {
+ $sel->select_ok($param, "label=$value");
+ }
+ else {
+ ok(0, "Unknown parameter type: $type");
+ }
+ }
+ else {
+ # If the value is undefined, then the param name is
+ # the ID of the radio button.
+ $sel->click_ok($param);
+ }
+ }
+ $sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes");
+ $sel->wait_for_page_to_load_ok(WAIT_TIME);
+ $sel->title_is("Parameters Updated");
+ }
+}
+
+1;
+
+__END__