From 9b6ec1f545da1cc4088ddf9cc117747954e58e65 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 26 Feb 2016 17:57:55 +0000 Subject: Bug 1069799 - move the QA repository into the main repository r=LpSolit --- docker_files/runtests.sh | 33 +- t/Support/Files.pm | 2 +- xt/README | 18 - xt/config/checksetup_answers.txt | 25 + xt/config/generate_test_data.pl | 616 ++++++++++ xt/config/patch.diff | 18 + xt/config/selenium_test.conf | 46 + xt/extensions/QA/Config.pm | 22 + xt/extensions/QA/Extension.pm | 74 ++ xt/extensions/QA/lib/Util.pm | 28 + xt/extensions/QA/template/en/default/hook/README | 5 + .../en/default/pages/qa/email_in.html.tmpl | 7 + xt/extensions/QA/template/en/default/qa/README | 16 + .../QA/template/en/default/qa/create_bug.txt.tmpl | 17 + .../en/default/qa/create_bug_with_headers.txt.tmpl | 33 + .../QA/template/en/default/qa/results.html.tmpl | 28 + .../QA/template/en/default/qa/update_bug.txt.tmpl | 13 + .../en/default/qa/update_bug_with_headers.txt.tmpl | 29 + xt/extensions/QA/web/README | 7 + xt/lib/Bugzilla/Test/Search.pm | 987 ---------------- xt/lib/Bugzilla/Test/Search/AndTest.pm | 52 - xt/lib/Bugzilla/Test/Search/Constants.pm | 1203 -------------------- xt/lib/Bugzilla/Test/Search/CustomTest.pm | 101 -- xt/lib/Bugzilla/Test/Search/FieldTest.pm | 617 ---------- xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm | 104 -- xt/lib/Bugzilla/Test/Search/InjectionTest.pm | 77 -- xt/lib/Bugzilla/Test/Search/NotTest.pm | 61 - xt/lib/Bugzilla/Test/Search/OperatorTest.pm | 103 -- xt/lib/Bugzilla/Test/Search/OrTest.pm | 141 --- xt/lib/QA/REST.pm | 65 ++ xt/lib/QA/RPC.pm | 289 +++++ xt/lib/QA/RPC/JSONRPC.pm | 174 +++ xt/lib/QA/RPC/XMLRPC.pm | 26 + xt/lib/QA/Tests.pm | 115 ++ xt/lib/QA/Util.pm | 372 ++++++ xt/rest/bugzilla.t | 60 + xt/rest/classification.t | 61 + xt/search.t | 82 -- xt/selenium/bug_edit.t | 441 +++++++ xt/selenium/choose_priority.t | 30 + xt/selenium/classifications.t | 142 +++ xt/selenium/config.t | 48 + xt/selenium/create_user_accounts.t | 139 +++ xt/selenium/custom_fields.t | 462 ++++++++ xt/selenium/custom_fields_admin.t | 56 + xt/selenium/dependencies.t | 56 + xt/selenium/edit_products_properties.t | 338 ++++++ xt/selenium/email_preferences.t | 405 +++++++ xt/selenium/enter_new_bug.t | 35 + xt/selenium/flags.t | 441 +++++++ xt/selenium/flags2.t | 308 +++++ xt/selenium/groups.t | 378 ++++++ xt/selenium/keywords.t | 181 +++ xt/selenium/login.t | 37 + xt/selenium/milestones.t | 149 +++ xt/selenium/password_complexity.t | 123 ++ xt/selenium/private_attachments.t | 173 +++ xt/selenium/qa_contact.t | 164 +++ xt/selenium/require_login.t | 83 ++ xt/selenium/sanity_check.t | 49 + xt/selenium/saved_searches.t | 117 ++ xt/selenium/search.t | 71 ++ xt/selenium/security.t | 198 ++++ xt/selenium/shared_searches.t | 199 ++++ xt/selenium/show_all_products.t | 56 + xt/selenium/shutdown.t | 77 ++ xt/selenium/status_whiteboard.t | 118 ++ xt/selenium/strict_isolation.t | 145 +++ xt/selenium/sudo_sessions.t | 158 +++ xt/selenium/target_milestones.t | 111 ++ xt/selenium/time_summary.t | 101 ++ xt/selenium/user_groups.t | 249 ++++ xt/selenium/user_matching.t | 188 +++ xt/selenium/user_preferences.t | 225 ++++ xt/selenium/user_privs.t | 60 + xt/selenium/votes.t | 233 ++++ xt/webservice/bug_add_attachment.t | 231 ++++ xt/webservice/bug_add_comment.t | 173 +++ xt/webservice/bug_attachments.t | 155 +++ xt/webservice/bug_comments.t | 178 +++ xt/webservice/bug_create.t | 243 ++++ xt/webservice/bug_fields.t | 223 ++++ xt/webservice/bug_get.t | 150 +++ xt/webservice/bug_history.t | 33 + xt/webservice/bug_legal_values.t | 104 ++ xt/webservice/bug_search.t | 211 ++++ xt/webservice/bug_update.t | 705 ++++++++++++ xt/webservice/bug_update_see_also.t | 86 ++ xt/webservice/bugzilla.t | 49 + xt/webservice/group_create.t | 101 ++ xt/webservice/jsonp.t | 34 + xt/webservice/product_create.t | 167 +++ xt/webservice/product_get.t | 113 ++ xt/webservice/user_create.t | 118 ++ xt/webservice/user_get.t | 222 ++++ xt/webservice/user_login_logout.t | 128 +++ xt/webservice/user_offer_account_by_email.t | 63 + 97 files changed, 12187 insertions(+), 3571 deletions(-) delete mode 100644 xt/README create mode 100644 xt/config/checksetup_answers.txt create mode 100755 xt/config/generate_test_data.pl create mode 100644 xt/config/patch.diff create mode 100644 xt/config/selenium_test.conf create mode 100644 xt/extensions/QA/Config.pm create mode 100644 xt/extensions/QA/Extension.pm create mode 100644 xt/extensions/QA/lib/Util.pm create mode 100644 xt/extensions/QA/template/en/default/hook/README create mode 100644 xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl create mode 100644 xt/extensions/QA/template/en/default/qa/README create mode 100644 xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl create mode 100644 xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl create mode 100644 xt/extensions/QA/template/en/default/qa/results.html.tmpl create mode 100644 xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl create mode 100644 xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl create mode 100644 xt/extensions/QA/web/README delete mode 100644 xt/lib/Bugzilla/Test/Search.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/AndTest.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/Constants.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/CustomTest.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/FieldTest.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/InjectionTest.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/NotTest.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/OperatorTest.pm delete mode 100644 xt/lib/Bugzilla/Test/Search/OrTest.pm create mode 100644 xt/lib/QA/REST.pm create mode 100644 xt/lib/QA/RPC.pm create mode 100644 xt/lib/QA/RPC/JSONRPC.pm create mode 100644 xt/lib/QA/RPC/XMLRPC.pm create mode 100644 xt/lib/QA/Tests.pm create mode 100644 xt/lib/QA/Util.pm create mode 100644 xt/rest/bugzilla.t create mode 100644 xt/rest/classification.t delete mode 100644 xt/search.t create mode 100644 xt/selenium/bug_edit.t create mode 100644 xt/selenium/choose_priority.t create mode 100644 xt/selenium/classifications.t create mode 100644 xt/selenium/config.t create mode 100644 xt/selenium/create_user_accounts.t create mode 100644 xt/selenium/custom_fields.t create mode 100644 xt/selenium/custom_fields_admin.t create mode 100644 xt/selenium/dependencies.t create mode 100644 xt/selenium/edit_products_properties.t create mode 100644 xt/selenium/email_preferences.t create mode 100644 xt/selenium/enter_new_bug.t create mode 100644 xt/selenium/flags.t create mode 100644 xt/selenium/flags2.t create mode 100644 xt/selenium/groups.t create mode 100644 xt/selenium/keywords.t create mode 100644 xt/selenium/login.t create mode 100644 xt/selenium/milestones.t create mode 100644 xt/selenium/password_complexity.t create mode 100644 xt/selenium/private_attachments.t create mode 100644 xt/selenium/qa_contact.t create mode 100644 xt/selenium/require_login.t create mode 100644 xt/selenium/sanity_check.t create mode 100644 xt/selenium/saved_searches.t create mode 100644 xt/selenium/search.t create mode 100644 xt/selenium/security.t create mode 100644 xt/selenium/shared_searches.t create mode 100644 xt/selenium/show_all_products.t create mode 100644 xt/selenium/shutdown.t create mode 100644 xt/selenium/status_whiteboard.t create mode 100644 xt/selenium/strict_isolation.t create mode 100644 xt/selenium/sudo_sessions.t create mode 100644 xt/selenium/target_milestones.t create mode 100644 xt/selenium/time_summary.t create mode 100644 xt/selenium/user_groups.t create mode 100644 xt/selenium/user_matching.t create mode 100644 xt/selenium/user_preferences.t create mode 100644 xt/selenium/user_privs.t create mode 100644 xt/selenium/votes.t create mode 100644 xt/webservice/bug_add_attachment.t create mode 100644 xt/webservice/bug_add_comment.t create mode 100644 xt/webservice/bug_attachments.t create mode 100644 xt/webservice/bug_comments.t create mode 100644 xt/webservice/bug_create.t create mode 100644 xt/webservice/bug_fields.t create mode 100644 xt/webservice/bug_get.t create mode 100644 xt/webservice/bug_history.t create mode 100644 xt/webservice/bug_legal_values.t create mode 100644 xt/webservice/bug_search.t create mode 100644 xt/webservice/bug_update.t create mode 100644 xt/webservice/bug_update_see_also.t create mode 100644 xt/webservice/bugzilla.t create mode 100644 xt/webservice/group_create.t create mode 100644 xt/webservice/jsonp.t create mode 100644 xt/webservice/product_create.t create mode 100644 xt/webservice/product_get.t create mode 100644 xt/webservice/user_create.t create mode 100644 xt/webservice/user_get.t create mode 100644 xt/webservice/user_login_logout.t create mode 100644 xt/webservice/user_offer_account_by_email.t diff --git a/docker_files/runtests.sh b/docker_files/runtests.sh index c15dca9ee..04152189f 100755 --- a/docker_files/runtests.sh +++ b/docker_files/runtests.sh @@ -41,11 +41,6 @@ if [ "$TEST_SUITE" = "docs" ]; then exit $? fi -echo -e "\n== Cloning QA test suite" -cd $BUGZILLA_ROOT -echo "Cloning git repo $GITHUB_QA_GIT branch $GITHUB_BASE_BRANCH ..." -git clone $GITHUB_QA_GIT -b $GITHUB_BASE_BRANCH qa - echo -e "\n== Starting database" /usr/bin/mysqld_safe & sleep 3 @@ -55,32 +50,24 @@ echo -e "\n== Starting memcached" sleep 3 echo -e "\n== Updating configuration" -sed -e "s?%DB%?$BUGS_DB_DRIVER?g" --in-place qa/config/checksetup_answers.txt -sed -e "s?%DB_NAME%?bugs_test?g" --in-place qa/config/checksetup_answers.txt -sed -e "s?%USER%?$USER?g" --in-place qa/config/checksetup_answers.txt -sed -e "s?%TRAVIS_BUILD_DIR%?$BUGZILLA_ROOT?g" --in-place qa/config/selenium_test.conf -echo "\$answer{'memcached_servers'} = 'localhost:11211';" >> qa/config/checksetup_answers.txt - -if [ "$TEST_SUITE" == "checksetup" ]; then - cd $BUGZILLA_ROOT/qa - /bin/bash /docker_files/buildbot_step "Checksetup" ./test_checksetup.pl config/config-checksetup-$BUGS_DB_DRIVER - exit $? -fi +sed -e "s?%DB%?$BUGS_DB_DRIVER?g" --in-place xt/config/checksetup_answers.txt +echo "\$answer{'memcached_servers'} = 'localhost:11211';" >> xt/config/checksetup_answers.txt echo -e "\n== Running checksetup" cd $BUGZILLA_ROOT -./checksetup.pl qa/config/checksetup_answers.txt -./checksetup.pl qa/config/checksetup_answers.txt +./checksetup.pl xt/config/checksetup_answers.txt +./checksetup.pl xt/config/checksetup_answers.txt echo -e "\n== Generating test data" -cd $BUGZILLA_ROOT/qa/config -perl -I../../local/lib/perl5 generate_test_data.pl +cd $BUGZILLA_ROOT/xt/config +perl generate_test_data.pl echo -e "\n== Starting web server" sed -e "s?^#Perl?Perl?" --in-place /etc/httpd/conf.d/bugzilla.conf /usr/sbin/httpd & sleep 3 +cd $BUGZILLA_ROOT if [ "$TEST_SUITE" = "selenium" ]; then export DISPLAY=:0 @@ -100,13 +87,11 @@ if [ "$TEST_SUITE" = "selenium" ]; then # but no tests actually executed. [ $NO_TESTS ] && exit 0 - cd $BUGZILLA_ROOT/qa/t - /bin/bash /docker_files/buildbot_step "Selenium" prove -f -v -I$BUGZILLA_ROOT/lib test_*.t + /bin/bash /docker_files/buildbot_step "Selenium" prove -f -v xt/selenium/*.t exit $? fi if [ "$TEST_SUITE" = "webservices" ]; then - cd $BUGZILLA_ROOT/qa/t - /bin/bash /docker_files/buildbot_step "Webservices" prove -f -v -I$BUGZILLA_ROOT/lib {rest,webservice}_*.t + /bin/bash /docker_files/buildbot_step "Webservices" prove -f -v xt/{rest,webservice}/*.t exit $? fi diff --git a/t/Support/Files.pm b/t/Support/Files.pm index 39bacccfc..e06cda738 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -28,7 +28,7 @@ foreach my $extension (@extensions) { find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, $extension); } -our @test_files = glob('t/*.t'); +our @test_files = glob('t/*.t xt/*/*.t'); sub isTestingFile { my ($file) = @_; diff --git a/xt/README b/xt/README deleted file mode 100644 index 22f9f171b..000000000 --- a/xt/README +++ /dev/null @@ -1,18 +0,0 @@ -The tests in this directory require a working database, as opposed -to the tests in t/, which simply test the code without a working -installation. - -Some of the tests may modify your current working installation, even -if only temporarily. To run the tests that modify your database, -set the environment variable BZ_WRITE_TESTS to 1. - -Some tests also take additional, optional arguments. You can pass arguments -to tests like: - - prove xt/search.t :: --long --operators=equals,notequals - -Note the "::"--that is necessary to note that the arguments are going to -the test, not to "prove". - -See the perldoc of the individual tests to see what options they support, -or do "perl xt/search.t --help". diff --git a/xt/config/checksetup_answers.txt b/xt/config/checksetup_answers.txt new file mode 100644 index 000000000..54473d3ed --- /dev/null +++ b/xt/config/checksetup_answers.txt @@ -0,0 +1,25 @@ + $answer{'db_host'} = 'localhost'; + $answer{'db_driver'} = '%DB%'; + $answer{'db_port'} = 0; + $answer{'db_name'} = 'bugs_test', + $answer{'db_user'} = 'bugs'; + $answer{'db_pass'} = 'bugs'; + $answer{'db_sock'} = ''; + $answer{'db_check'} = 1; + $answer{'db_mysql_ssl_ca_file'} = ''; + $answer{'db_mysql_ssl_ca_path'} = ''; + $answer{'db_mysql_ssl_client_cert'} = ''; + $answer{'db_mysql_ssl_client_key'} = ''; + $answer{'urlbase'} = 'http://localhost/bugzilla/'; + $answer{'create_htaccess'} = ''; + $answer{'use_suexec'} = ''; + $answer{'index_html'} = 0; + $answer{'cvsbin'} = '/usr/bin/cvs'; + $answer{'interdiffbin'} = '/usr/bin/interdiff'; + $answer{'diffpath'} = '/usr/bin'; + $answer{'webservergroup'} = 'bugzilla'; + $answer{'ADMIN_OK'} = 'Y'; + $answer{'ADMIN_EMAIL'} = 'admin@bugzilla.org'; + $answer{'ADMIN_PASSWORD'} = 'password'; + $answer{'ADMIN_REALNAME'} = 'QA Admin'; + $answer{'NO_PAUSE'} = 1; diff --git a/xt/config/generate_test_data.pl b/xt/config/generate_test_data.pl new file mode 100755 index 000000000..ab186698d --- /dev/null +++ b/xt/config/generate_test_data.pl @@ -0,0 +1,616 @@ +#!/usr/bin/perl + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +# -*- Mode: perl; indent-tabs-mode: nil -*- + +use 5.10.1; +use strict; +use warnings; + +use Cwd; + +my $conf_path; +my $config; + +BEGIN { + say 'reading the config file...'; + my $conf_file = 'selenium_test.conf'; + $config = do "$conf_file" + or die "can't read configuration '$conf_file': $!$@"; + + $conf_path = $config->{bugzilla_path}; + + # We don't want randomly-generated keys. We want the ones specified + # in the config file so that we can use them in tests scripts. + *Bugzilla::User::APIKey::_check_api_key = sub { return $_[1]; }; +} + +use lib $conf_path; + +use Bugzilla; +use Bugzilla::Attachment; +use Bugzilla::Bug; +use Bugzilla::User; +use Bugzilla::Install; +use Bugzilla::Milestone; +use Bugzilla::Product; +use Bugzilla::Component; +use Bugzilla::Group; +use Bugzilla::Version; +use Bugzilla::Constants; +use Bugzilla::Keyword; +use Bugzilla::Config qw(:admin); +use Bugzilla::User::Setting; +use Bugzilla::User::APIKey; + +my $dbh = Bugzilla->dbh; + +# set Bugzilla usage mode to USAGE_MODE_CMDLINE +Bugzilla->usage_mode(USAGE_MODE_CMDLINE); + +########################################################################## +# Set Parameters +########################################################################## + +# Some parameters must be turned on to create bugs requiring them. +# They are also expected to be turned on by some webservice_*.t scripts. +my ($urlbase, $sslbase); +$urlbase = $config->{browser_url} . '/' . $config->{bugzilla_installation}; +$urlbase .= '/' unless $urlbase =~ /\/$/; + +if ($urlbase =~ /^https/) { + $sslbase = $urlbase; + $urlbase =~ s/^https(.+)$/http$1/; +} + +my %set_params = ( + urlbase => $urlbase, + sslbase => $sslbase, + useqacontact => 1, + mail_delivery_method => 'Test', + maxattachmentsize => 256, +); + +my $params_modified; +foreach my $param (keys %set_params) { + my $value = $set_params{$param}; + next unless defined $value && Bugzilla->params->{$param} ne $value; + SetParam($param, $value); + $params_modified = 1; +} + +write_params() if $params_modified; + +########################################################################## +# Set Default User Preferences +########################################################################## + +# When editing a bug, the page being displayed depends on the +# post_bug_submit_action user pref. We set it globally so that we know +# the exact behavior of process_bug.cgi. +my %user_prefs = (post_bug_submit_action => 'nothing'); + +foreach my $pref (keys %user_prefs) { + my $value = $user_prefs{$pref}; + Bugzilla::User::Setting::set_default($pref, $value, 0); +} + +########################################################################## +# Create Users +########################################################################## + +# First of all, remove the default .* regexp for the editbugs group. +my $group = Bugzilla::Group->new({ name => 'editbugs' }); +$group->set_user_regexp(''); +$group->update(); + +my @usernames = ( + 'admin', 'no-privs', + 'QA-Selenium-TEST', 'canconfirm', + 'tweakparams', 'permanent_user', + 'editbugs', 'disabled', +); + +say 'creating user accounts...'; +foreach my $username (@usernames) { + my ($password, $login); + + my $prefix = $username; + if ($username eq 'permanent_user') { + $password = $config->{admin_user_passwd}; + $login = $config->{$username}; + } + elsif ($username eq 'no-privs') { + $prefix = 'unprivileged'; + } + elsif ($username eq 'QA-Selenium-TEST') { + $prefix = 'QA_Selenium_TEST'; + } + + $password ||= $config->{"${prefix}_user_passwd"}; + $login ||= $config->{"${prefix}_user_login"}; + my $api_key = $config->{"${prefix}_user_api_key"}; + + if (is_available_username($login)) { + my %extra_args; + if ($username eq 'disabled') { + $extra_args{disabledtext} = '!!This is the text!!'; + } + + my $user = Bugzilla::User->create( + { login_name => $login, + realname => $username, + cryptpassword => $password, + %extra_args, + } + ); + + if ($api_key) { + Bugzilla::User::APIKey->create( + { user_id => $user->id, + description => 'API key for QA tests', + api_key => $api_key } + ); + } + + if ($username eq 'admin' or $username eq 'permanent_user') { + Bugzilla::Install::make_admin($login); + } + } +} + +########################################################################## +# Create Bugs +########################################################################## + +# login to bugzilla +my $admin_user = Bugzilla::User->check($config->{admin_user_login}); +Bugzilla->set_user($admin_user); + +my %field_values = ( + 'priority' => 'Highest', + 'bug_status' => 'CONFIRMED', + 'version' => 'unspecified', + 'bug_file_loc' => '', + 'comment' => 'please ignore this bug', + 'component' => 'TestComponent', + 'rep_platform' => 'All', + 'short_desc' => 'This is a testing bug only', + 'product' => 'TestProduct', + 'op_sys' => 'Linux', + 'bug_severity' => 'normal', +); + +say 'creating bugs...'; +my $bug = Bugzilla::Bug->create( \%field_values ); +say 'Bug ' . $bug->id . ' created'; +if (Bugzilla::Bug->new('public_bug')->{error}) { + # The deadline must be set so that this bug can be used to test + # timetracking fields using WebServices. + $bug = Bugzilla::Bug->create({ %field_values, alias => 'public_bug', deadline => '2010-01-01' }); + say 'Bug ' . $bug->id . ' (alias: public_bug) created'; +} + +########################################################################## +# Create Classifications +########################################################################## + +my @classifications = ({ name => 'Class2_QA', + description => "required by Selenium... DON'T DELETE" }, +); + +say 'creating classifications...'; +for my $class (@classifications) { + my $new_class = Bugzilla::Classification->new({ name => $class->{name} }); + if (!$new_class) { + $dbh->do('INSERT INTO classifications (name, description) VALUES (?, ?)', + undef, ($class->{name}, $class->{description})); + } +} +########################################################################## +# Create Products +########################################################################## + +my @products = ( + { product_name => 'QA-Selenium-TEST', + description => "used by Selenium test.. DON'T DELETE", + versions => ['unspecified', 'QAVersion'], + milestones => ['QAMilestone'], + defaultmilestone => '---', + components => [ + { name => 'QA-Selenium-TEST', + description => "used by Selenium test.. DON'T DELETE", + initialowner => $config->{QA_Selenium_TEST_user_login}, + initialqacontact => $config->{QA_Selenium_TEST_user_login}, + initial_cc => [$config->{QA_Selenium_TEST_user_login}], + } + ], + }, + + { product_name => 'Another Product', + description => 'Alternate product used by Selenium. Do not edit!', + versions => ['unspecified', 'Another1', 'Another2'], + milestones => ['AnotherMS1', 'AnotherMS2', 'Milestone'], + defaultmilestone => '---', + components => [ + { name => 'c1', + description => 'c1', + initialowner => $config->{permanent_user}, + initialqacontact => '', + initial_cc => [], + }, + { name => 'c2', + description => 'c2', + initialowner => $config->{permanent_user}, + initialqacontact => '', + initial_cc => [], + }, + ], + }, + + { product_name => 'C2 Forever', + description => 'I must remain in the Class2_QA classification ' . + 'in all cases! Do not edit!', + classification => 'Class2_QA', + versions => ['unspecified', 'C2Ver'], + milestones => ['C2Mil'], + defaultmilestone => '---', + components => [ + { name => 'Helium', + description => 'Feel free to add bugs to me', + initialowner => $config->{permanent_user}, + initialqacontact => '', + initial_cc => [], + } + ], + }, + + { product_name => 'QA Entry Only', + description => 'Only the QA group may enter bugs here.', + versions => ['unspecified'], + milestones => [], + defaultmilestone => '---', + components => [ + { name => 'c1', + description => "Same name as Another Product's component", + initialowner => $config->{QA_Selenium_TEST_user_login}, + initialqacontact => '', + initial_cc => [], + } + ], + }, + + { product_name => 'QA Search Only', + description => 'Only the QA group may search for bugs here.', + versions => ['unspecified'], + milestones => [], + defaultmilestone => '---', + components => [ + { name => 'c1', + description => 'Still same name as the Another component', + initialowner => $config->{QA_Selenium_TEST_user_login}, + initialqacontact => '', + initial_cc => [], + } + ], + }, +); + +say 'creating products...'; +foreach my $product (@products) { + my $new_product = Bugzilla::Product->new({ name => $product->{product_name} }); + if (!$new_product) { + my $class_id = 1; + if ($product->{classification}) { + $class_id = Bugzilla::Classification->new({ name => $product->{classification} })->id; + } + $dbh->do('INSERT INTO products (name, description, classification_id) VALUES (?, ?, ?)', + undef, ($product->{product_name}, $product->{description}, $class_id)); + + $new_product = Bugzilla::Product->new({ name => $product->{product_name} }); + + $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?, ?)', + undef, ($new_product->id, $product->{defaultmilestone} )); + + # Now clear the internal list of accessible products. + delete Bugzilla->user->{selectable_products}; + + foreach my $component (@{ $product->{components} }) { + Bugzilla::Component->create( + { name => $component->{name}, + product => $new_product, + description => $component->{description}, + initialowner => $component->{initialowner}, + initialqacontact => $component->{initialqacontact}, + initial_cc => $component->{initial_cc}, + } + ); + } + } + + foreach my $version (@{ $product->{versions} }) { + my $new_version = Bugzilla::Version->new({ name => $version, product => $new_product }); + if (!$new_version) { + Bugzilla::Version->create({ value => $version, product => $new_product }); + } + } + + foreach my $milestone (@{ $product->{milestones} }) { + my $new_milestone = Bugzilla::Milestone->new({ name => $milestone, product => $new_product }); + if (!$new_milestone) { + # We don't use Bugzilla::Milestone->create because we want to + # bypass security checks. + $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?,?)', + undef, $new_product->id, $milestone); + } + } +} + +########################################################################## +# Create Groups +########################################################################## + +# create Master group +my ($group_name, $group_desc) = ('Master', 'Master Selenium Group DO NOT EDIT!'); + +say 'creating groups...'; +my $new_group = Bugzilla::Group->new({ name => $group_name }); +if (!$new_group) { + my $group = Bugzilla::Group->create({ name => $group_name, + description => $group_desc, + isbuggroup => 1}); + + $dbh->do('INSERT INTO group_control_map + (group_id, product_id, entry, membercontrol, othercontrol, canedit) + SELECT ?, products.id, 0, ?, ?, 0 FROM products', + undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPSHOWN)); +} + +# create QA-Selenium-TEST group. Do not use Group->create() so that +# the admin group doesn't inherit membership (yes, that's what we want!). +($group_name, $group_desc) = ('QA-Selenium-TEST', "used by Selenium test.. DON'T DELETE"); + +$new_group = Bugzilla::Group->new({ name => $group_name }); +if (!$new_group) { + $dbh->do('INSERT INTO groups (name, description, isbuggroup, isactive) + VALUES (?, ?, 1, 1)', undef, ($group_name, $group_desc)); +} + +########################################################################## +# Add Users to Groups +########################################################################## + +my @users_groups = ( + { user => $config->{QA_Selenium_TEST_user_login}, group => 'QA-Selenium-TEST' }, + { user => $config->{tweakparams_user_login}, group => 'tweakparams' }, + { user => $config->{canconfirm_user_login}, group => 'canconfirm' }, + { user => $config->{editbugs_user_login}, group => 'editbugs' }, +); + +say 'adding users to groups...'; +foreach my $user_group (@users_groups) { + my $group = Bugzilla::Group->new({ name => $user_group->{group} }); + my $user = Bugzilla::User->new({ name => $user_group->{user} }); + + my $sth_add_mapping = + $dbh->prepare('INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)'); + # Don't crash if the entry already exists. + eval { $sth_add_mapping->execute($user->id, $group->id, 0, GRANT_DIRECT); }; +} + +########################################################################## +# Associate Products with groups +########################################################################## + +# Associate the QA-Selenium-TEST group with the QA-Selenium-TEST. +my $created_group = Bugzilla::Group->new({ name => 'QA-Selenium-TEST' }); +my $secret_product = Bugzilla::Product->new({ name => 'QA-Selenium-TEST' }); +my $no_entry = Bugzilla::Product->new({ name => 'QA Entry Only' }); +my $no_search = Bugzilla::Product->new({ name => 'QA Search Only' }); + +say 'restricting products to groups...'; +# Don't crash if the entries already exist. +my $sth = $dbh->prepare('INSERT INTO group_control_map + (group_id, product_id, entry, membercontrol, othercontrol, canedit) + VALUES (?, ?, ?, ?, ?, ?)'); +eval { $sth->execute($created_group->id, $secret_product->id, 1, CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0); }; +eval { $sth->execute($created_group->id, $no_entry->id, 1, CONTROLMAPNA, CONTROLMAPNA, 0); }; +eval { $sth->execute($created_group->id, $no_search->id, 0, CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0); }; + +########################################################################## +# Create flag types +########################################################################## + +my @flagtypes = ( + {name => 'spec_multi_flag', desc => 'Specifically requestable and multiplicable bug flag', + is_requestable => 1, is_requesteeble => 1, is_multiplicable => 1, grant_group => 'editbugs', + target_type => 'b', cc_list => '', inclusions => ['Another Product:c1']}, +); + +say 'creating flag types...'; +foreach my $flag (@flagtypes) { + # The name is not unique, even within a single product/component, so there is NO WAY + # to know if the existing flag type is the one we want or not. + # As our Selenium scripts would be confused anyway if there is already such a flag name, + # we simply skip it and assume the existing flag type is the one we want. + next if Bugzilla::FlagType->new({ name => $flag->{name} }); + + my $grant_group_id = $flag->{grant_group} ? Bugzilla::Group->new({ name => $flag->{grant_group} })->id : undef; + my $request_group_id = $flag->{request_group} ? Bugzilla::Group->new({ name => $flag->{request_group} })->id : undef; + + $dbh->do('INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable, + is_requesteeble, is_multiplicable, grant_group_id, request_group_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + undef, ($flag->{name}, $flag->{desc}, $flag->{cc_list}, $flag->{target_type}, + $flag->{is_requestable}, $flag->{is_requesteeble}, $flag->{is_multiplicable}, + $grant_group_id, $request_group_id)); + + my $type_id = $dbh->bz_last_key('flagtypes', 'id'); + + foreach my $inclusion (@{$flag->{inclusions}}) { + my ($product, $component) = split(':', $inclusion); + my ($prod_id, $comp_id); + if ($product) { + my $prod_obj = Bugzilla::Product->new({ name => $product }); + $prod_id = $prod_obj->id; + if ($component) { + $comp_id = Bugzilla::Component->new({ name => $component, product => $prod_obj})->id; + } + } + $dbh->do('INSERT INTO flaginclusions (type_id, product_id, component_id) + VALUES (?, ?, ?)', + undef, ($type_id, $prod_id, $comp_id)); + } +} + +########################################################################## +# Create custom fields +########################################################################## + +my @fields = ( + { name => 'cf_QA_status', + description => 'QA Status', + type => FIELD_TYPE_MULTI_SELECT, + sortkey => 100, + mailhead => 0, + enter_bug => 1, + obsolete => 0, + custom => 1, + values => ['verified', 'in progress', 'untested'] + }, + { name => 'cf_single_select', + description => 'SingSel', + type => FIELD_TYPE_SINGLE_SELECT, + mailhead => 0, + enter_bug => 1, + custom => 1, + values => [qw(one two three)], + }, +); + +say 'creating custom fields...'; +foreach my $f (@fields) { + # Skip existing custom fields. + next if Bugzilla::Field->new({ name => $f->{name} }); + + my @values; + if (exists $f->{values}) { + @values = @{$f->{values}}; + # We have to delete this key, else create() will complain + # that 'values' is not an existing column name. + delete $f->{values}; + } + my $field = Bugzilla::Field->create($f); + + # Now populate the table with valid values, if necessary. + next unless scalar @values; + + my $sth = $dbh->prepare('INSERT INTO ' . $field->name . ' (value) VALUES (?)'); + foreach my $value (@values) { + $sth->execute($value); + } +} + +#################################################################### +# Set Parameters That Require Other Things To Have Been Done First # +#################################################################### + +if (Bugzilla->params->{insidergroup} ne 'QA-Selenium-TEST') { + SetParam('insidergroup', 'QA-Selenium-TEST'); + write_params(); +} + +if (Bugzilla->params->{timetrackinggroup} ne 'editbugs') { + SetParam('timetrackinggroup', 'editbugs'); + write_params(); +} + +######################## +# Create a Private Bug # +######################## + +my $test_user = Bugzilla::User->check($config->{QA_Selenium_TEST_user_login}); +$test_user->{'groups'} = [ + Bugzilla::Group->new({ name => 'editbugs' }), + Bugzilla::Group->new({ name => 'QA-Selenium-TEST' }) +]; # editbugs is needed for alias creation +Bugzilla->set_user($test_user); + +if (Bugzilla::Bug->new('private_bug')->{error}) { + say 'Creating private bug...'; + my %priv_values = %field_values; + $priv_values{alias} = 'private_bug'; + $priv_values{product} = 'QA-Selenium-TEST'; + $priv_values{component} = 'QA-Selenium-TEST'; + my $bug = Bugzilla::Bug->create(\%priv_values); + say 'Bug ' . $bug->id . ' (alias: private_bug) created'; +} + +###################### +# Create Attachments # +###################### + +say 'creating attachments...'; +# We use the contents of this script as the attachment. +open(my $attachment_fh, '<', __FILE__) or die __FILE__ . ": $!"; +my $attachment_contents; +{ + local $/; + $attachment_contents = <$attachment_fh>; +} +close($attachment_fh); + +foreach my $alias (qw(public_bug private_bug)) { + my $bug = Bugzilla::Bug->new($alias); + + foreach my $is_private (0, 1) { + Bugzilla::Attachment->create({ + bug => $bug, + data => $attachment_contents, + description => "${alias}_${is_private}", + filename => "${alias}_${is_private}.pl", + mimetype => 'application/x-perl', + isprivate => $is_private, + }); + } +} + +################### +# Create Keywords # +################### + +my @keywords = ( + { name => 'test-keyword-1', + description => 'Created for Bugzilla QA Tests, Keyword 1' }, + { name => 'test-keyword-2', + description => 'Created for Bugzilla QA Tests, Keyword 2' }, +); + +say 'creating keywords...'; +foreach my $kw (@keywords) { + next if Bugzilla::Keyword->new({ name => $kw->{name} }); + Bugzilla::Keyword->create($kw); +} + +############################ +# Install the QA extension # +############################ + +say 'copying the QA extension...'; +my $output = `cp -R ../extensions/QA $conf_path/extensions/.`; +print $output if $output; + +my $cwd = cwd(); +chdir($conf_path); +$output = `perl contrib/fixperms.pl`; +print $output if $output; +chdir($cwd); + +say 'installation and configuration complete!'; diff --git a/xt/config/patch.diff b/xt/config/patch.diff new file mode 100644 index 000000000..cbaff0249 --- /dev/null +++ b/xt/config/patch.diff @@ -0,0 +1,18 @@ +Index: Bugzilla/Config/MTA.pm +=================================================================== +RCS file: /cvsroot/mozilla/webtools/bugzilla/Bugzilla/Config/MTA.pm,v +retrieving revision 1.13 +diff -3 -p -u -r1.13 MTA.pm +--- Bugzilla/Config/MTA.pm 13 Nov 2006 23:32:28 -0000 1.13 ++++ Bugzilla/Config/MTA.pm 9 Dec 2006 12:19:44 -0000 +@@ -44,7 +44,9 @@ sub get_param_list { + { + name => 'mail_delivery_method', + type => 's', +- choices => [Email::Send->new()->all_mailers(), 'None'], ++ # Bugzilla is not ready yet to send mails to newsgroups, and 'IO' ++ # is of no use for now as we already have our own 'Test' mode. ++ choices => [grep {$_ ne 'NNTP' && $_ ne 'IO'} Email::Send->new()->all_mailers(), 'None'], + default => 'Sendmail', + checker => \&check_mail_delivery_method + }, diff --git a/xt/config/selenium_test.conf b/xt/config/selenium_test.conf new file mode 100644 index 000000000..e4d7e4417 --- /dev/null +++ b/xt/config/selenium_test.conf @@ -0,0 +1,46 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +# To make this configuration file useful to you, you have to: +# - set the path and URL to your Bugzilla installation. +# - replace @my.company by something more relevant to you, +# also what comes before @my.company if you want/need to. +# - set passwords for each user accounts. + +{ 'browser' => '*firefox', + 'experimental_browser_launcher' => '*chrome', + 'host' => 'localhost', + 'port' => 4444, + 'browser_url' => 'http://localhost', + 'attachment_file' => '/home/bugzilla/devel/htdocs/bugzilla/xt/config/patch.diff', + 'bugzilla_installation' => 'bugzilla', + 'bugzilla_path' => '/home/bugzilla/devel/htdocs/bugzilla', + 'admin_user_login' => 'admin@my.company', + 'admin_user_passwd' => '******', + 'admin_user_username' => 'admin', + 'admin_user_api_key' => 'zQ5TSBzq7tTZMtKYq9K1ZqJMjifKx3cPL7pIGk9Q', + 'permanent_user' => 'permanent_user@my.company', + 'unprivileged_user_login' => 'no-privs@my.company', + 'unprivileged_user_passwd' => '******', + 'unprivileged_user_username' => 'no-privs', + 'unprivileged_user_login_truncated' => 'no-privs@my', + 'unprivileged_user_api_key' => 'zQ5TSBzqP4nrdBKYq9Re4qJrjifKx3cK07pIGk9Q', + 'QA_Selenium_TEST_user_login' => 'QA-Selenium-TEST@my.company', + 'QA_Selenium_TEST_user_passwd' => '******', + 'editbugs_user_login' => 'editbugs@my.company', + 'editbugs_user_passwd' => '******', + 'editbugs_user_api_key' => 'zQ5ewBzq3gTrdBKYq9K1ZqJMjifKx3cKleE6k9TQ', + 'canconfirm_user_login' => 'canconfirm@my.company', + 'canconfirm_user_passwd' => '******', + 'tweakparams_user_login' => 'tweakparams@my.company', + 'tweakparams_user_login_truncated' => 'tweakparams@my', + 'tweakparams_user_passwd' => '******', + 'disabled_user_login' => 'disabled@my.company', + 'disabled_user_passwd' => '******', + 'common_email' => '@my.company', + 'test_extensions' => 0, +}; diff --git a/xt/extensions/QA/Config.pm b/xt/extensions/QA/Config.pm new file mode 100644 index 000000000..b4f6bc9a2 --- /dev/null +++ b/xt/extensions/QA/Config.pm @@ -0,0 +1,22 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::QA; + +use 5.10.1; +use strict; +use warnings; + +use constant NAME => 'QA'; + +use constant REQUIRED_MODULES => [ +]; + +use constant OPTIONAL_MODULES => [ +]; + +__PACKAGE__->NAME; diff --git a/xt/extensions/QA/Extension.pm b/xt/extensions/QA/Extension.pm new file mode 100644 index 000000000..5befe3e36 --- /dev/null +++ b/xt/extensions/QA/Extension.pm @@ -0,0 +1,74 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::QA; + +use 5.10.1; +use strict; +use warnings; + +use base qw(Bugzilla::Extension); + +use Bugzilla::Extension::QA::Util; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Util; +use Bugzilla::Bug; +use Bugzilla::User; + +our $VERSION = '1.0'; + +sub page_before_template { + my ($self, $args) = @_; + return if $args->{page_id} ne 'qa/email_in.html'; + + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + print $cgi->header; + + # Needed to make sure he can access and edit bugs. + my $user = Bugzilla::User->check($cgi->param('sender')); + Bugzilla->set_user($user); + + my ($output, $tmpl_file); + my $action = $cgi->param('action') || ''; + my $vars = { sender => $user, action => $action, pid => $$ }; + + if ($action eq 'create') { + $tmpl_file = 'qa/create_bug.txt.tmpl'; + } + elsif ($action eq 'create_with_headers') { + $tmpl_file = 'qa/create_bug_with_headers.txt.tmpl'; + } + elsif ($action =~ /^update(_with_headers)?$/) { + my $f = $1 || ''; + $tmpl_file = "qa/update_bug$f.txt.tmpl"; + my $bug = Bugzilla::Bug->check($cgi->param('bug_id')); + $vars->{bug_id} = $bug->id; + } + else { + ThrowUserError('unknown_action', { action => $action }); + } + + $template->process($tmpl_file, $vars, \$output) + or ThrowTemplateError($template->error()); + + my $file = "/tmp/email_in_$$.txt"; + open(FH, '>', $file); + print FH $output; + close FH; + + $output = `email_in.pl -v < $file 2>&1`; + unlink $file; + + parse_output($output, $vars); + + $template->process('qa/results.html.tmpl', $vars) + or ThrowTemplateError($template->error()); +} + +__PACKAGE__->NAME; diff --git a/xt/extensions/QA/lib/Util.pm b/xt/extensions/QA/lib/Util.pm new file mode 100644 index 000000000..e299adcc9 --- /dev/null +++ b/xt/extensions/QA/lib/Util.pm @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::QA::Util; + +use 5.10.1; +use strict; +use warnings; + +use base qw(Exporter); + +our @EXPORT = qw( + parse_output +); + +sub parse_output { + my ($output, $vars) = @_; + + $vars->{error} = ($output =~ /software error/i) ? 1 : 0; + $vars->{output} = $output; + $vars->{bug_id} ||= ($output =~ /Created bug (\d+)/i) ? $1 : undef; +} + +1; diff --git a/xt/extensions/QA/template/en/default/hook/README b/xt/extensions/QA/template/en/default/hook/README new file mode 100644 index 000000000..3f1e487e2 --- /dev/null +++ b/xt/extensions/QA/template/en/default/hook/README @@ -0,0 +1,5 @@ +Template hooks go in this directory. Template hooks are called in normal +Bugzilla templates like [% Hook.process('some-hook') %]. +More information about them can be found in the documentation of +Bugzilla::Extension. (Do "perldoc Bugzilla::Extension" from the main +Bugzilla directory to see that documentation.) diff --git a/xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl b/xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl new file mode 100644 index 000000000..bcb75107d --- /dev/null +++ b/xt/extensions/QA/template/en/default/pages/qa/email_in.html.tmpl @@ -0,0 +1,7 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] diff --git a/xt/extensions/QA/template/en/default/qa/README b/xt/extensions/QA/template/en/default/qa/README new file mode 100644 index 000000000..604d00cfe --- /dev/null +++ b/xt/extensions/QA/template/en/default/qa/README @@ -0,0 +1,16 @@ +Normal templates go in this directory. You can load them in your +code like this: + +use Bugzilla::Error; +my $template = Bugzilla->template; +$template->process('qa/some-template.html.tmpl') + or ThrowTemplateError($template->error()); + +That would be how to load a file called some-template.html.tmpl that +was in this directory. + +Note that you have to be careful that the full path of your template +never conflicts with a template that exists in Bugzilla or in +another extension, or your template might override that template. That's why +we created this directory called 'qa' for you, so you +can put your templates in here to help avoid conflicts. diff --git a/xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl b/xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl new file mode 100644 index 000000000..5a83a6c5b --- /dev/null +++ b/xt/extensions/QA/template/en/default/qa/create_bug.txt.tmpl @@ -0,0 +1,17 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +From: [% sender.email %] +Subject: [% terms.Bug %] created using email_in.pl +Content-Type: text/plain; charset="UTF-8" + +@product = TestProduct +@component = TestComponent +@version = unspecified + +This [% terms.bug %] has been created using email_in.pl (PID: [% pid %]). diff --git a/xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl b/xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl new file mode 100644 index 000000000..997378343 --- /dev/null +++ b/xt/extensions/QA/template/en/default/qa/create_bug_with_headers.txt.tmpl @@ -0,0 +1,33 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +From - Sat Jan 1 18:38:17 2011 +X-Account-Key: account2 +X-UIDL: GmailId12d42784d83cb4a4 +X-Mozilla-Status: 0011 +X-Mozilla-Status2: 00000000 +X-Mozilla-Keys: +Return-Path: +Received: from [192.168.0.2] (provider.com [51.162.153.14]) + by mx.google.com with ESMTPS id m10sm12712256wbc.4.2011.01.01.09.38.01 + (version=TLSv1/SSLv3 cipher=RC4-MD5); + Sat, 01 Jan 2011 09:38:01 -0800 (PST) +Message-ID: <4D1F6580.9060076@gmail.com> +Date: Sat, 01 Jan 2011 18:38:08 +0100 +User-Agent: Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.2.13) Gecko/20101207 Lightning/1.0b2 Thunderbird/3.1.7 +MIME-Version: 1.0 +From: [% sender.email %] +Subject: [% terms.Bug %] created using email_in.pl (with email headers) +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: 8bit + +@product = TestProduct +@component = TestComponent +@version = unspecified + +This [% terms.bug %] has been created using email_in.pl (PID: [% pid %]) with email headers. diff --git a/xt/extensions/QA/template/en/default/qa/results.html.tmpl b/xt/extensions/QA/template/en/default/qa/results.html.tmpl new file mode 100644 index 000000000..a2f812697 --- /dev/null +++ b/xt/extensions/QA/template/en/default/qa/results.html.tmpl @@ -0,0 +1,28 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% title = BLOCK %] + [% IF error %] + Unexpected error + [% ELSE %] + email_in.pl output + [% END %] +[% END %] + +[% PROCESS global/header.html.tmpl %] + +

Action '[% action FILTER html %]' successful

+ +
+

PID: [% pid FILTER html %]

+

[%+ terms.Bug %] ID: [% bug_id FILTER html %]

+ +

Full output:

+
[% output FILTER html_light %]
+ +[% PROCESS global/footer.html.tmpl %] diff --git a/xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl b/xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl new file mode 100644 index 000000000..f37c00262 --- /dev/null +++ b/xt/extensions/QA/template/en/default/qa/update_bug.txt.tmpl @@ -0,0 +1,13 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +From: [% sender.email %] +Subject: [[% terms.Bug %] [%+ bug_id %]] This subject is ignored, only the [% terms.bug %] ID matters +Content-Type: text/plain; charset="UTF-8" + +Comment added by email_in.pl (PID: [% pid %]). No other changes. diff --git a/xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl b/xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl new file mode 100644 index 000000000..fd093d3b0 --- /dev/null +++ b/xt/extensions/QA/template/en/default/qa/update_bug_with_headers.txt.tmpl @@ -0,0 +1,29 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +From - Sat Jan 1 18:38:17 2011 +X-Account-Key: account2 +X-UIDL: GmailId12d42784d83cb4a4 +X-Mozilla-Status: 0011 +X-Mozilla-Status2: 00000000 +X-Mozilla-Keys: +Return-Path: +Received: from [192.168.0.2] (provider.com [51.162.153.14]) + by mx.google.com with ESMTPS id m10sm12712256wbc.4.2011.01.01.09.38.01 + (version=TLSv1/SSLv3 cipher=RC4-MD5); + Sat, 01 Jan 2011 09:38:01 -0800 (PST) +Message-ID: <4D1F6580.9060076@gmail.com> +Date: Sat, 01 Jan 2011 18:38:08 +0100 +User-Agent: Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.2.13) Gecko/20101207 Lightning/1.0b2 Thunderbird/3.1.7 +MIME-Version: 1.0 +From: [% sender.email %] +Subject: [[% terms.Bug %] [%+ bug_id %]] This subject is ignored, only the [% terms.bug %] ID matters +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: 8bit + +Comment added by email_in.pl (PID: [% pid %]) with email headers. No other changes. diff --git a/xt/extensions/QA/web/README b/xt/extensions/QA/web/README new file mode 100644 index 000000000..23456410f --- /dev/null +++ b/xt/extensions/QA/web/README @@ -0,0 +1,7 @@ +Web-accessible files, like JavaScript, CSS, and images go in this +directory. You can reference them directly in your HTML. For example, +if you have a file called "style.css" and your extension is called +"Foo", you would put it in "extensions/Foo/web/style.css", and then +you could link to it in HTML like: + + \ No newline at end of file diff --git a/xt/lib/Bugzilla/Test/Search.pm b/xt/lib/Bugzilla/Test/Search.pm deleted file mode 100644 index ca3bba5cf..000000000 --- a/xt/lib/Bugzilla/Test/Search.pm +++ /dev/null @@ -1,987 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This module tests Bugzilla/Search.pm. It uses various constants -# that are in Bugzilla::Test::Search::Constants, in xt/lib/. -# -# It does this by: -# 1) Creating a bunch of field values. Each field value is -# randomly named and fully unique. -# 2) Creating a bunch of bugs that use those unique field -# values. Each bug has different characteristics--see -# the comment above the NUM_BUGS constant for a description -# of each bug. -# 3) Running searches using the combination of every search operator against -# every field. The tests that we run are described by the TESTS constant. -# Some of the operator/field combinations are known to be broken-- -# these are listed in the KNOWN_BROKEN constant. -# 4) For each search, we make sure that certain bugs are contained in -# the search, and certain other bugs are not contained in the search. -# The code for the operator/field tests is mostly in -# Bugzilla::Test::Search::FieldTest. -# 5) After testing each operator/field combination's functionality, we -# do additional tests to make sure that there are no SQL injections -# possible via any operator/field combination. The code for the -# SQL Injection tests is in Bugzilla::Test::Search::InjectionTest. -# -# Generally, the only way that you should modify the behavior of this -# script is by modifying the constants. - -package Bugzilla::Test::Search; - -use strict; -use warnings; -use Bugzilla::Attachment; -use Bugzilla::Bug (); -use Bugzilla::Constants; -use Bugzilla::Field; -use Bugzilla::Field::Choice; -use Bugzilla::FlagType; -use Bugzilla::Group; -use Bugzilla::Install (); -use Bugzilla::Test::Search::Constants; -use Bugzilla::Test::Search::CustomTest; -use Bugzilla::Test::Search::FieldTestNormal; -use Bugzilla::Test::Search::OperatorTest; -use Bugzilla::User (); -use Bugzilla::Util qw(generate_random_password); - -use Carp; -use DateTime; -use Scalar::Util qw(blessed); - -############### -# Constructor # -############### - -sub new { - my ($class, $options) = @_; - return bless { options => $options }, $class; -} - -############# -# Accessors # -############# - -sub options { return $_[0]->{options} } -sub option { return $_[0]->{options}->{$_[1]} } - -sub num_tests { - my ($self) = @_; - my @top_operators = $self->top_level_operators; - my @all_operators = $self->all_operators; - my $top_operator_tests = $self->_total_operator_tests(\@top_operators); - my $all_operator_tests = $self->_total_operator_tests(\@all_operators); - - my @fields = $self->all_fields; - - # Basically, we run TESTS_PER_RUN tests for each field/operator combination. - my $top_combinations = $top_operator_tests * scalar(@fields); - my $all_combinations = $all_operator_tests * scalar(@fields); - # But we also have ORs, for which we run combinations^2 tests. - my $join_tests = $self->option('long') - ? ($top_combinations * $all_combinations) : 0; - # And AND tests, which means we run 2x $join_tests; - $join_tests = $join_tests * 2; - # Also, because of NOT tests and Normal tests, we run 3x $top_combinations. - my $basic_tests = $top_combinations * 3; - my $operator_field_tests = ($basic_tests + $join_tests) * TESTS_PER_RUN; - - # Then we test each field/operator combination for SQL injection. - my @injection_values = INJECTION_TESTS; - my $sql_injection_tests = scalar(@fields) * scalar(@top_operators) - * scalar(@injection_values) * NUM_SEARCH_TESTS; - - # This @{ [] } thing is the only reasonable way to get a count out of a - # constant array. - my $special_tests = scalar(@{ [SPECIAL_PARAM_TESTS, CUSTOM_SEARCH_TESTS] }) - * TESTS_PER_RUN; - - return $operator_field_tests + $sql_injection_tests + $special_tests; -} - -sub _total_operator_tests { - my ($self, $operators) = @_; - - # Some operators have more than one test. Find those ones and add - # them to the total operator tests - my $extra_operator_tests; - foreach my $operator (@$operators) { - my $tests = TESTS->{$operator}; - next if !$tests; - my $extra_num = scalar(@$tests) - 1; - $extra_operator_tests += $extra_num; - } - return scalar(@$operators) + $extra_operator_tests; - -} - -sub all_operators { - my ($self) = @_; - if (not $self->{all_operators}) { - - my @operators; - if (my $limit_operators = $self->option('operators')) { - @operators = split(',', $limit_operators); - } - else { - @operators = sort (keys %{ Bugzilla::Search::OPERATORS() }); - } - # "substr" is just a backwards-compatibility operator, same as "substring". - @operators = grep { $_ ne 'substr' } @operators; - $self->{all_operators} = \@operators; - } - return @{ $self->{all_operators} }; -} - -sub all_fields { - my $self = shift; - if (not $self->{all_fields}) { - $self->_create_custom_fields(); - my @fields = @{ Bugzilla->fields }; - @fields = sort { $a->name cmp $b->name } @fields; - $self->{all_fields} = \@fields; - } - return @{ $self->{all_fields} }; -} - -sub top_level_operators { - my ($self) = @_; - if (!$self->{top_level_operators}) { - my @operators; - my $limit_top = $self->option('top-operators'); - if ($limit_top) { - @operators = split(',', $limit_top); - } - else { - @operators = $self->all_operators; - } - $self->{top_level_operators} = \@operators; - } - return @{ $self->{top_level_operators} }; -} - -sub text_fields { - my ($self) = @_; - my @text_fields = grep { $_->type == FIELD_TYPE_TEXTAREA - or $_->type == FIELD_TYPE_FREETEXT } $self->all_fields; - @text_fields = map { $_->name } @text_fields; - push(@text_fields, qw(short_desc status_whiteboard bug_file_loc see_also)); - return @text_fields; -} - -sub bugs { - my $self = shift; - $self->{bugs} ||= [map { $self->_create_one_bug($_) } (1..NUM_BUGS)]; - return @{ $self->{bugs} }; -} - -# Get a numbered bug. -sub bug { - my ($self, $number) = @_; - return ($self->bugs)[$number - 1]; -} - -sub admin { - my $self = shift; - if (!$self->{admin_user}) { - my $admin = create_user("admin"); - Bugzilla::Install::make_admin($admin); - $self->{admin_user} = $admin; - } - # We send back a fresh object every time, to make sure that group - # memberships are always up-to-date. - return new Bugzilla::User($self->{admin_user}->id); -} - -sub nobody { - my $self = shift; - $self->{nobody} ||= Bugzilla::Group->create({ name => "nobody-" . random(), - description => "Nobody", isbuggroup => 1 }); - return $self->{nobody}; -} -sub everybody { - my ($self) = @_; - $self->{everybody} ||= create_group('To The Limit'); - return $self->{everybody}; -} - -sub bug_create_value { - my ($self, $number, $field) = @_; - $field = $field->name if blessed($field); - if ($number == 6 and $field ne 'alias') { - $number = 1; - } - my $extra_values = $self->_extra_bug_create_values->{$number}; - if (exists $extra_values->{$field}) { - return $extra_values->{$field}; - } - return $self->_bug_create_values->{$number}->{$field}; -} -sub bug_update_value { - my ($self, $number, $field) = @_; - $field = $field->name if blessed($field); - if ($number == 6 and $field ne 'alias') { - $number = 1; - } - return $self->_bug_update_values->{$number}->{$field}; -} - -# Values used to create the bugs. -sub _bug_create_values { - my $self = shift; - return $self->{bug_create_values} if $self->{bug_create_values}; - my %values; - foreach my $number (1..NUM_BUGS) { - $values{$number} = $self->_create_field_values($number, 'for create'); - } - $self->{bug_create_values} = \%values; - return $self->{bug_create_values}; -} -# Values as they existed on the bug, at creation time. Used by the -# changedfrom tests. -sub _extra_bug_create_values { - my $self = shift; - $self->{extra_bug_create_values} ||= { map { $_ => {} } (1..NUM_BUGS) }; - return $self->{extra_bug_create_values}; -} - -# Values used to update the bugs after they are created. -sub _bug_update_values { - my $self = shift; - return $self->{bug_update_values} if $self->{bug_update_values}; - my %values; - foreach my $number (1..NUM_BUGS) { - $values{$number} = $self->_create_field_values($number); - } - $self->{bug_update_values} = \%values; - return $self->{bug_update_values}; -} - -############################## -# General Helper Subroutines # -############################## - -sub random { - $_[0] ||= FIELD_SIZE; - generate_random_password(@_); -} - -# We need to use a custom timestamp for each create() and update(), -# because the database returns the same value for LOCALTIMESTAMP(0) -# for the entire transaction, and we need each created bug to have -# its own creation_ts and delta_ts. -sub timestamp { - my ($day, $second) = @_; - return DateTime->new( - year => 2037, - month => 1, - day => $day, - hour => 12, - minute => $second, - second => 0, - # We make it floating because the timezone doesn't matter for our uses, - # and we want totally consistent behavior across all possible machines. - time_zone => 'floating', - ); -} - -sub create_keyword { - my ($number) = @_; - return Bugzilla::Keyword->create({ - name => "$number-keyword-" . random(), - description => "Keyword $number" }); -} - -sub create_user { - my ($prefix) = @_; - my $user_name = $prefix . '-' . random(15) . "@" . random(12) - . "." . random(3); - my $user_realname = $prefix . '-' . random(); - my $user = Bugzilla::User->create({ - login_name => $user_name, - realname => $user_realname, - cryptpassword => '*', - }); - return $user; -} - -sub create_group { - my ($prefix) = @_; - return Bugzilla::Group->create({ - name => "$prefix-group-" . random(), description => "Everybody $prefix", - userregexp => '.*', isbuggroup => 1 }); -} - -sub create_legal_value { - my ($field, $number) = @_; - my $type = Bugzilla::Field::Choice->type($field); - my $field_name = $field->name; - return $type->create({ value => "$number-$field_name-" . random(), - is_open => 0 }); -} - -######################### -# Custom Field Creation # -######################### - -sub _create_custom_fields { - my ($self) = @_; - return if !$self->option('add-custom-fields'); - - while (my ($type, $name) = each %{ CUSTOM_FIELDS() }) { - my $exists = new Bugzilla::Field({ name => $name }); - next if $exists; - Bugzilla::Field->create({ - name => $name, - type => $type, - description => "Search Test Field $name", - enter_bug => 1, - custom => 1, - buglist => 1, - is_mandatory => 0, - }); - } -} - -######################## -# Field Value Creation # -######################## - -sub _create_field_values { - my ($self, $number, $for_create) = @_; - my $dbh = Bugzilla->dbh; - - Bugzilla->set_user($self->admin); - - my @selects = grep { $_->is_select } $self->all_fields; - my %values; - foreach my $field (@selects) { - next if $field->is_abnormal; - $values{$field->name} = create_legal_value($field, $number)->name; - } - - my $group = create_group($number); - $values{groups} = [$group->name]; - - $values{'keywords'} = create_keyword($number)->name; - - foreach my $field (qw(assigned_to qa_contact reporter cc)) { - $values{$field} = create_user("$number-$field")->login; - } - - my $classification = Bugzilla::Classification->create( - { name => "$number-classification-" . random() }); - $classification = $classification->name; - - my $version = "$number-version-" . random(); - my $milestone = "$number-tm-" . random(15); - my $product = Bugzilla::Product->create({ - name => "$number-product-" . random(), - description => 'Created by t/search.t', - defaultmilestone => $milestone, - classification => $classification, - version => $version, - allows_unconfirmed => 1, - }); - foreach my $item ($group, $self->nobody) { - $product->set_group_controls($item, - { membercontrol => CONTROLMAPSHOWN, - othercontrol => CONTROLMAPNA }); - } - # $product->update() is called lower down. - my $component = Bugzilla::Component->create({ - product => $product, name => "$number-component-" . random(), - initialowner => create_user("$number-defaultowner")->login, - initialqacontact => create_user("$number-defaultqa")->login, - initial_cc => [create_user("$number-initcc")->login], - description => "Component $number" }); - - $values{'product'} = $product->name; - $values{'component'} = $component->name; - $values{'target_milestone'} = $milestone; - $values{'version'} = $version; - - foreach my $field ($self->text_fields) { - # We don't add a - after $field for the text fields, because - # if we do, fulltext searching for short_desc pulls out - # "short_desc" as a word and matches it in every bug. - my $value = "$number-$field" . random(); - if ($field eq 'bug_file_loc' or $field eq 'see_also') { - $value = "http://$value-" . random(3) - . "/show_bug.cgi?id=$number"; - } - $values{$field} = $value; - } - $values{'tag'} = ["$number-tag-" . random()]; - - my @date_fields = grep { $_->type == FIELD_TYPE_DATETIME } $self->all_fields; - foreach my $field (@date_fields) { - # We use 03 as the month because that differs from our creation_ts, - # delta_ts, and deadline. (It's nice to have recognizable values - # for each field when debugging.) - my $second = $for_create ? $number : $number + 1; - $values{$field->name} = "2037-03-0$number 12:34:0$second"; - } - - $values{alias} = "$number-alias-" . random(12); - - # Prefixing the original comment with "description" makes the - # lesserthan and greaterthan tests behave predictably. - my $comm_prefix = $for_create ? "description-" : ''; - $values{comment} = "$comm_prefix$number-comment-" . random() - . ' ' . random(); - - my @flags; - my $setter = create_user("$number-setters.login_name"); - my $requestee = create_user("$number-requestees.login_name"); - $values{set_flags} = _create_flags($number, $setter, $requestee); - - my $month = $for_create ? "12" : "02"; - $values{'deadline'} = "2037-$month-0$number"; - my $estimate_times = $for_create ? 10 : 1; - $values{estimated_time} = $estimate_times * $number; - - $values{attachment} = _get_attach_values($number, $for_create); - - # Some things only happen on the first bug. - if ($number == 1) { - # We use 6 as the prefix for the extra values, because bug 6's values - # don't otherwise get used (since bug 6 is created as a clone of - # bug 1). This also makes sure that our greaterthan/lessthan - # tests work properly. - my $extra_group = create_group(6); - $product->set_group_controls($extra_group, - { membercontrol => CONTROLMAPSHOWN, - othercontrol => CONTROLMAPNA }); - $values{groups} = [$values{groups}->[0], $extra_group->name]; - my $extra_keyword = create_keyword(6); - $values{keywords} = [$values{keywords}, $extra_keyword->name]; - my $extra_cc = create_user("6-cc"); - $values{cc} = [$values{cc}, $extra_cc->login]; - my @multi_selects = grep { $_->type == FIELD_TYPE_MULTI_SELECT } - $self->all_fields; - foreach my $field (@multi_selects) { - my $new_value = create_legal_value($field, 6); - my $name = $field->name; - $values{$name} = [$values{$name}, $new_value->name]; - } - push(@{ $values{'tag'} }, "6-tag-" . random()); - } - - # On bug 5, any field that *can* be left empty, *is* left empty. - if ($number == 5) { - my @set_fields = grep { $_->type == FIELD_TYPE_SINGLE_SELECT } - $self->all_fields; - @set_fields = map { $_->name } @set_fields; - push(@set_fields, qw(short_desc version reporter)); - foreach my $key (keys %values) { - delete $values{$key} unless grep { $_ eq $key } @set_fields; - } - } - - $product->update(); - - return \%values; -} - -# Flags -sub _create_flags { - my ($number, $setter, $requestee) = @_; - - my $flagtypes = _create_flagtypes($number); - - my %flags; - foreach my $type (qw(a b)) { - $flags{$type} = _get_flag_values(@_, $flagtypes->{$type}); - } - return \%flags; -} - -sub _create_flagtypes { - my ($number) = @_; - my $dbh = Bugzilla->dbh; - my $name = "$number-flag-" . random(); - my $desc = "FlagType $number"; - - my %flagtypes; - foreach my $target (qw(a b)) { - $dbh->do("INSERT INTO flagtypes - (name, description, target_type, is_requestable, - is_requesteeble, is_multiplicable, cc_list) - VALUES (?,?,?,1,1,1,'')", - undef, $name, $desc, $target); - my $id = $dbh->bz_last_key('flagtypes', 'id'); - $dbh->do('INSERT INTO flaginclusions (type_id) VALUES (?)', - undef, $id); - my $flagtype = new Bugzilla::FlagType($id); - $flagtypes{$target} = $flagtype; - } - return \%flagtypes; -} - -sub _get_flag_values { - my ($number, $setter, $requestee, $flagtype) = @_; - - my @set_flags; - if ($number <= 2) { - foreach my $value (qw(? - + ?)) { - my $flag = { type_id => $flagtype->id, status => $value, - setter => $setter, flagtype => $flagtype }; - push(@set_flags, $flag); - } - $set_flags[0]->{requestee} = $requestee->login; - } - else { - @set_flags = ({ type_id => $flagtype->id, status => '+', - setter => $setter, flagtype => $flagtype }); - } - return \@set_flags; -} - -# Attachments -sub _get_attach_values { - my ($number, $for_create) = @_; - - my $boolean = $number == 1 ? 1 : 0; - if ($for_create) { - $boolean = !$boolean ? 1 : 0; - } - my $ispatch = $for_create ? 'ispatch' : 'is_patch'; - my $isobsolete = $for_create ? 'isobsolete' : 'is_obsolete'; - my $isprivate = $for_create ? 'isprivate' : 'is_private'; - my $mimetype = $for_create ? 'mimetype' : 'content_type'; - - my %values = ( - description => "$number-attach_desc-" . random(), - filename => "$number-filename-" . random(), - $ispatch => $boolean, - $isobsolete => $boolean, - $isprivate => $boolean, - $mimetype => "text/x-$number-" . random(), - ); - if ($for_create) { - $values{data} = "$number-data-" . random() . random(); - } - return \%values; -} - -################ -# Bug Creation # -################ - -sub _create_one_bug { - my ($self, $number) = @_; - my $dbh = Bugzilla->dbh; - - # We need bug 6 to have a unique alias that is not a clone of bug 1's, - # so we get the alias separately from the other parameters. - my $alias = $self->bug_create_value($number, 'alias'); - my $update_alias = $self->bug_update_value($number, 'alias'); - - # Otherwise, make bug 6 a clone of bug 1. - my $real_number = $number; - $number = 1 if $number == 6; - - my $reporter = $self->bug_create_value($number, 'reporter'); - Bugzilla->set_user(Bugzilla::User->check($reporter)); - - # We create the bug with one set of values, and then we change it - # to have different values. - my %params = %{ $self->_bug_create_values->{$number} }; - $params{alias} = $alias; - - # There are some things in bug_create_values that shouldn't go into - # create(). - delete @params{qw(attachment set_flags tag)}; - - my ($status, $resolution, $see_also) = - delete @params{qw(bug_status resolution see_also)}; - # All the bugs are created with everconfirmed = 0. - $params{bug_status} = 'UNCONFIRMED'; - my $bug = Bugzilla::Bug->create(\%params); - - # These are necessary for the changedfrom tests. - my $extra_values = $self->_extra_bug_create_values->{$number}; - foreach my $field (qw(comments remaining_time percentage_complete - keyword_objects everconfirmed dependson blocked - groups_in classification actual_time)) - { - $extra_values->{$field} = $bug->$field; - } - $extra_values->{reporter_accessible} = $number == 1 ? 0 : 1; - $extra_values->{cclist_accessible} = $number == 1 ? 0 : 1; - - if ($number == 5) { - # Bypass Bugzilla::Bug--we don't want any changes in bugs_activity - # for bug 5. - $dbh->do('UPDATE bugs SET qa_contact = NULL, reporter_accessible = 0, - cclist_accessible = 0 WHERE bug_id = ?', - undef, $bug->id); - $dbh->do('DELETE FROM cc WHERE bug_id = ?', undef, $bug->id); - my $ts = '1970-01-01 00:00:00'; - $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? - WHERE bug_id = ?', undef, $ts, $ts, $bug->id); - $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', - undef, $ts, $bug->id); - $bug->{creation_ts} = $ts; - $extra_values->{see_also} = []; - } - else { - # Manually set the creation_ts so that each bug has a different one. - # - # Also, manually update the resolution and bug_status, because - # we want to see both of them change in bugs_activity, so we - # have to start with values for both (and as of the time when I'm - # writing this test, Bug->create doesn't support setting resolution). - # - # Same for see_also. - my $timestamp = timestamp($number, $number - 1); - my $creation_ts = $timestamp->ymd . ' ' . $timestamp->hms; - $bug->{creation_ts} = $creation_ts; - $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', - undef, $creation_ts, $bug->id); - $dbh->do('UPDATE bugs SET creation_ts = ?, bug_status = ?, - resolution = ? WHERE bug_id = ?', - undef, $creation_ts, $status, $resolution, $bug->id); - $dbh->do('INSERT INTO bug_see_also (bug_id, value, class) VALUES (?,?,?)', - undef, $bug->id, $see_also, 'Bugzilla::BugUrl::Bugzilla'); - $extra_values->{see_also} = $bug->see_also; - - # All the tags must be created as the admin user, so that the - # admin user can find them, later. - my $original_user = Bugzilla->user; - Bugzilla->set_user($self->admin); - my $tags = $self->bug_create_value($number, 'tag'); - $bug->add_tag($_) foreach @$tags; - $extra_values->{tags} = $tags; - Bugzilla->set_user($original_user); - - if ($number == 1) { - # Bug 1 needs to start off with reporter_accessible and - # cclist_accessible being 0, so that when we change them to 1, - # that change shows up in bugs_activity. - $dbh->do('UPDATE bugs SET reporter_accessible = 0, - cclist_accessible = 0 WHERE bug_id = ?', - undef, $bug->id); - # Bug 1 gets three comments, so that longdescs.count matches it - # uniquely. The third comment is added in the middle, so that the - # last comment contains all of the important data, like work_time. - $bug->add_comment("1-comment-" . random(100)); - } - - my %update_params = %{ $self->_bug_update_values->{$number} }; - my %reverse_map = reverse %{ Bugzilla::Bug->FIELD_MAP }; - foreach my $db_name (keys %reverse_map) { - next if $db_name eq 'comment'; - next if $db_name eq 'status_whiteboard'; - if (exists $update_params{$db_name}) { - my $update_name = $reverse_map{$db_name}; - $update_params{$update_name} = delete $update_params{$db_name}; - } - } - - my ($new_status, $new_res) = - delete @update_params{qw(status resolution)}; - # Bypass the status workflow. - $bug->{bug_status} = $new_status; - $bug->{resolution} = $new_res; - $bug->{everconfirmed} = 1 if $number == 1; - - # add/remove/set fields. - $update_params{keywords} = { set => $update_params{keywords} }; - $update_params{groups} = { add => $update_params{groups}, - remove => $bug->groups_in }; - my @cc_remove = map { $_->login } @{ $bug->cc_users }; - my $cc_new = $update_params{cc}; - my @cc_add = ref($cc_new) ? @$cc_new : ($cc_new); - # We make the admin an explicit CC on bug 1 (but not on bug 6), so - # that we can test the %user% pronoun properly. - if ($real_number == 1) { - push(@cc_add, $self->admin->login); - } - $update_params{cc} = { add => \@cc_add, remove => \@cc_remove }; - my $see_also_remove = $bug->see_also; - my $see_also_add = [$update_params{see_also}]; - $update_params{see_also} = { add => $see_also_add, - remove => $see_also_remove }; - $update_params{comment} = { body => $update_params{comment} }; - $update_params{work_time} = $number; - # Setting work_time kills the remaining_time, so we need to - # preserve that. We add 8 because that produces an integer - # percentage_complete for bug 1, which is necessary for - # accurate "equals"-type searching. - $update_params{remaining_time} = $number + 8; - $update_params{reporter_accessible} = $number == 1 ? 1 : 0; - $update_params{cclist_accessible} = $number == 1 ? 1 : 0; - $update_params{alias} = $update_alias; - - $bug->set_all(\%update_params); - my $flags = $self->bug_create_value($number, 'set_flags')->{b}; - $bug->set_flags([], $flags); - $timestamp->set(second => $number); - $bug->update($timestamp->ymd . ' ' . $timestamp->hms); - $extra_values->{flags} = $bug->flags; - - # It's not generally safe to do update() multiple times on - # the same Bug object. - $bug = new Bugzilla::Bug($bug->id); - my $update_flags = $self->bug_update_value($number, 'set_flags')->{b}; - $_->{status} = 'X' foreach @{ $bug->flags }; - $bug->set_flags($bug->flags, $update_flags); - if ($number == 1) { - my $comment_id = $bug->comments->[-1]->id; - $bug->set_comment_is_private({ $comment_id => 1 }); - } - $bug->update($bug->delta_ts); - - my $attach_create = $self->bug_create_value($number, 'attachment'); - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $creation_ts, - %$attach_create }); - # Store for the changedfrom tests. - $extra_values->{attachments} = - [new Bugzilla::Attachment($attachment->id)]; - - my $attach_update = $self->bug_update_value($number, 'attachment'); - $attachment->set_all($attach_update); - # In order to keep the mimetype on the ispatch attachment, - # we need to bypass the validator. - $attachment->{mimetype} = $attach_update->{content_type}; - my $attach_flags = $self->bug_update_value($number, 'set_flags')->{a}; - $attachment->set_flags([], $attach_flags); - $attachment->update($bug->delta_ts); - } - - # Values for changedfrom. - $extra_values->{creation_ts} = $bug->creation_ts; - $extra_values->{delta_ts} = $bug->creation_ts; - - return new Bugzilla::Bug($bug->id); -} - -################################### -# Test::Builder Memory Efficiency # -################################### - -# Test::Builder stores information for each test run, but Test::Harness -# and TAP::Harness don't actually need this information. When we run 60 -# million tests, the history eats up all our memory. (After about -# 1 million tests, memory usage is around 1 GB.) -# -# The only part of the history that Test::More actually *uses* is the "ok" -# field, which we store more efficiently, in an array, and then we re-populate -# the Test_Results in Test::Builder at the end of the test. -sub clean_test_history { - my ($self) = @_; - return if !$self->option('long'); - my $builder = Test::More->builder; - my $current_test = $builder->current_test; - - # I don't use details() because I don't want to copy the array. - my $results = $builder->{Test_Results}; - my $check_test = $current_test - 1; - while (my $result = $results->[$check_test]) { - last if !$result; - $self->test_success($check_test, $result->{ok}); - $check_test--; - } - - # Truncate the test history array, but retain the current test number. - $builder->{Test_Results} = []; - $builder->{Curr_Test} = $current_test; -} - -sub test_success { - my ($self, $index, $status) = @_; - $self->{test_success}->[$index] = $status; - return $self->{test_success}; -} - -sub repopulate_test_results { - my ($self) = @_; - return if !$self->option('long'); - $self->clean_test_history(); - # We create only two hashes, for memory efficiency. - my %ok = ( ok => 1 ); - my %not_ok = ( ok => 0 ); - my @results; - foreach my $success (@{ $self->{test_success} }) { - push(@results, $success ? \%ok : \%not_ok); - } - my $builder = Test::More->builder; - $builder->{Test_Results} = \@results; -} - -########## -# Caches # -########## - -# When doing AND and OR tests, we essentially test the same field/operator -# combinations over and over. So, if we're going to be running those tests, -# we cache the translated_value of the FieldTests globally so that we don't -# have to re-run the value-translation code every time (which can be pretty -# slow). -sub value_translation_cache { - my ($self, $field_test, $value) = @_; - return if !$self->option('long'); - my $test_name = $field_test->name; - if (@_ == 3) { - $self->{value_translation_cache}->{$test_name} = $value; - } - return $self->{value_translation_cache}->{$test_name}; -} - -# When doing AND/OR tests, the value for transformed_value_was_equal -# (see Bugzilla::Test::Search::FieldTest) won't be recalculated -# if we pull our values from the value_translation_cache. So we need -# to also cache the values for transformed_value_was_equal. -sub was_equal_cache { - my ($self, $field_test, $number, $value) = @_; - return if !$self->option('long'); - my $test_name = $field_test->name; - if (@_ == 4) { - $self->{tvwe_cache}->{$test_name}->{$number} = $value; - } - return $self->{tvwe_cache}->{$test_name}->{$number}; -} - -############# -# Main Test # -############# - -sub run { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - # We want backtraces on any "die" message or any warning. - # Otherwise it's hard to trace errors inside of Bugzilla::Search from - # reading automated test run results. - local $SIG{__WARN__} = \&Carp::cluck; - local $SIG{__DIE__} = \&Carp::confess; - - $dbh->bz_start_transaction(); - - # Some parameters need to be set in order for the tests to function - # properly. - my $everybody = $self->everybody; - my $params = Bugzilla->params; - local $params->{'useclassification'} = 1; - local $params->{'useqacontact'} = 1; - local $params->{'usetargetmilestone'} = 1; - local $params->{'mail_delivery_method'} = 'None'; - local $params->{'timetrackinggroup'} = $everybody->name; - local $params->{'insidergroup'} = $everybody->name; - - $self->_setup_bugs(); - - # Even though _setup_bugs set us as an admin, we want to be sure at - # this point that we have an admin with refreshed group memberships. - Bugzilla->set_user($self->admin); - foreach my $test (CUSTOM_SEARCH_TESTS) { - my $custom_test = new Bugzilla::Test::Search::CustomTest($test, $self); - $custom_test->run(); - } - foreach my $test (SPECIAL_PARAM_TESTS) { - my $operator_test = - new Bugzilla::Test::Search::OperatorTest($test->{operator}, $self); - my $field = Bugzilla::Field->check($test->{field}); - my $special_test = new Bugzilla::Test::Search::FieldTestNormal( - $operator_test, $field, $test); - $special_test->run(); - } - foreach my $operator ($self->top_level_operators) { - my $operator_test = - new Bugzilla::Test::Search::OperatorTest($operator, $self); - $operator_test->run(); - } - - # Rollbacks won't get rid of bugs_fulltext entries, so we do that ourselves. - my @bug_ids = map { $_->id } $self->bugs; - my $bug_id_string = join(',', @bug_ids); - $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id IN ($bug_id_string)"); - $dbh->bz_rollback_transaction(); - $self->repopulate_test_results(); -} - -# This makes a few changes to the bugs after they're created--changes -# that can only be done after all the bugs have been created. -sub _setup_bugs { - my ($self) = @_; - $self->_setup_dependencies(); - $self->_set_bug_id_fields(); - $self->_protect_bug_6(); -} -sub _setup_dependencies { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - # Set up depedency relationships between the bugs. - # Bug 1 + 6 depend on bug 2 and block bug 3. - my $bug2 = $self->bug(2); - my $bug3 = $self->bug(3); - foreach my $number (1,6) { - my $bug = $self->bug($number); - my @original_delta = ($bug2->delta_ts, $bug3->delta_ts); - Bugzilla->set_user($bug->reporter); - $bug->set_dependencies([$bug2->id], [$bug3->id]); - $bug->update($bug->delta_ts); - # Setting dependencies changed the delta_ts on bug2 and bug3, so - # re-set them back to what they were before. However, we leave - # the correct update times in bugs_activity, so that the changed* - # searches still work right. - my $set_delta = $dbh->prepare( - 'UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); - foreach my $row ([$original_delta[0], $bug2->id], - [$original_delta[1], $bug3->id]) - { - $set_delta->execute(@$row); - } - } -} - -sub _set_bug_id_fields { - my ($self) = @_; - # BUG_ID fields couldn't be set before, because before we create bug 1, - # we don't necessarily have any valid bug ids.) - my @bug_id_fields = grep { $_->type == FIELD_TYPE_BUG_ID } - $self->all_fields; - foreach my $number (1..NUM_BUGS) { - my $bug = $self->bug($number); - $number = 1 if $number == 6; - next if $number == 5; - my $other_bug = $self->bug($number + 1); - Bugzilla->set_user($bug->reporter); - foreach my $field (@bug_id_fields) { - $bug->set_custom_field($field, $other_bug->id); - $bug->update($bug->delta_ts); - } - } -} - -sub _protect_bug_6 { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - Bugzilla->set_user($self->admin); - - # Put bug6 in the nobody group. - my $nobody = $self->nobody; - # We pull it newly from the DB to be sure it's safe to call update() - # on. - my $bug6 = new Bugzilla::Bug($self->bug(6)->id); - $bug6->add_group($nobody); - $bug6->update($bug6->delta_ts); - - # Remove the admin (and everybody else) from the $nobody group. - $dbh->do('DELETE FROM group_group_map - WHERE grantor_id = ? OR member_id = ?', undef, - $nobody->id, $nobody->id); -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/AndTest.pm b/xt/lib/Bugzilla/Test/Search/AndTest.pm deleted file mode 100644 index f34ba1f3a..000000000 --- a/xt/lib/Bugzilla/Test/Search/AndTest.pm +++ /dev/null @@ -1,52 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This test combines two field/operator combinations using AND in -# a single boolean chart. -package Bugzilla::Test::Search::AndTest; -use parent qw(Bugzilla::Test::Search::OrTest); - -use Bugzilla::Test::Search::Constants; -use List::MoreUtils qw(all); - -use constant type => 'AND'; - -############# -# Accessors # -############# - -# In an AND test, bugs ARE supposed to be contained only if they are contained -# by ALL tests. -sub bug_is_contained { - my ($self, $number) = @_; - return all { $_->bug_is_contained($number) } $self->field_tests; -} - -sub _bug_will_actually_be_contained { - my ($self, $number) = @_; - return all { $_->will_actually_contain_bug($number) } $self->field_tests; -} - -############################## -# Bugzilla::Search arguments # -############################## - -sub search_params { - my ($self) = @_; - my @all_params = map { $_->search_params } $self->field_tests; - my %params; - my $chart = 0; - foreach my $item (@all_params) { - $params{"field0-$chart-0"} = $item->{'field0-0-0'}; - $params{"type0-$chart-0"} = $item->{'type0-0-0'}; - $params{"value0-$chart-0"} = $item->{'value0-0-0'}; - $chart++; - } - return \%params; -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/Constants.pm b/xt/lib/Bugzilla/Test/Search/Constants.pm deleted file mode 100644 index 5d84ec6ff..000000000 --- a/xt/lib/Bugzilla/Test/Search/Constants.pm +++ /dev/null @@ -1,1203 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - - -# These are constants used by Bugzilla::Test::Search. -# See the comment at the top of that package for a general overview -# of how the search test works, and how the constants are used. -# More detailed information on each constant is available in the comments -# in this file. -package Bugzilla::Test::Search::Constants; -use parent qw(Exporter); -use Bugzilla::Constants; -use Bugzilla::Util qw(generate_random_password); - -our @EXPORT = qw( - ATTACHMENT_FIELDS - BROKEN_NOT - COLUMN_TRANSLATION - COMMENT_FIELDS - CUSTOM_FIELDS - CUSTOM_SEARCH_TESTS - FIELD_SIZE - FIELD_SUBSTR_SIZE - FLAG_FIELDS - INJECTION_BROKEN_FIELD - INJECTION_BROKEN_OPERATOR - INJECTION_TESTS - KNOWN_BROKEN - NUM_BUGS - NUM_SEARCH_TESTS - SKIP_FIELDS - SPECIAL_PARAM_TESTS - SUBSTR_NO_FIELD_ADD - SUBSTR_SIZE - TESTS - TESTS_PER_RUN - USER_FIELDS -); - -# Bug 1 is designed to be found by all the "equals" tests. It has -# multiple values for several fields where other fields only have -# one value. -# -# Bug 2 and 3 have a dependency relationship with Bug 1, -# but show up in "not equals" tests. We do use bug 2 in multiple-value -# tests. -# -# Bug 4 should never show up in any equals test, and has no relationship -# with any other bug. However, it does have all its fields set. -# -# Bug 5 only has values set for mandatory fields, to expose problems -# that happen with "not equals" tests failing to catch bugs that don't -# have a value set at all. -# -# Bug 6 is a clone of Bug 1, but is in a group that the searcher isn't -# in. -use constant NUM_BUGS => 6; - -# How many tests there are for each operator/field combination other -# than the "contains" tests. -use constant NUM_SEARCH_TESTS => 3; -# This is how many tests get run for each field/operator. -use constant TESTS_PER_RUN => NUM_SEARCH_TESTS + NUM_BUGS; - -# This is how many random characters we generate for most fields' names. -# (Some fields can't be this long, though, so they have custom lengths -# in Bugzilla::Test::Search). -use constant FIELD_SIZE => 30; - -# These are the custom fields that are created if the BZ_MODIFY_DATABASE_TESTS -# environment variable is set. -use constant CUSTOM_FIELDS => { - FIELD_TYPE_FREETEXT, 'cf_freetext', - FIELD_TYPE_SINGLE_SELECT, 'cf_single_select', - FIELD_TYPE_MULTI_SELECT, 'cf_multi_select', - FIELD_TYPE_TEXTAREA, 'cf_textarea', - FIELD_TYPE_DATETIME, 'cf_datetime', - FIELD_TYPE_BUG_ID, 'cf_bugid', -}; - -# This translates fielddefs names into Search column names. -use constant COLUMN_TRANSLATION => { - creation_ts => 'opendate', - delta_ts => 'changeddate', - work_time => 'actual_time', -}; - -# Make comment field names to their Bugzilla::Comment accessor. -use constant COMMENT_FIELDS => { - longdesc => 'body', - commenter => 'author', - 'longdescs.isprivate' => 'is_private', -}; - -# Same as above, for Bugzilla::Attachment. -use constant ATTACHMENT_FIELDS => { - mimetype => 'contenttype', - submitter => 'attacher', - thedata => 'data', -}; - -# Same, for Bugzilla::Flag. -use constant FLAG_FIELDS => { - 'flagtypes.name' => 'name', - 'setters.login_name' => 'setter', - 'requestees.login_name' => 'requestee', -}; - -# These are fields that we don't test. Test::More will mark these -# "TODO & SKIP", and not run tests for them at all. -# -# We don't support days_elapsed or owner_idle_time yet. -use constant SKIP_FIELDS => qw( - owner_idle_time - days_elapsed -); - -# All the fields that represent users. -use constant USER_FIELDS => qw( - assigned_to - cc - reporter - qa_contact - commenter - attachments.submitter - setters.login_name - requestees.login_name -); - -# For the "substr"-type searches, how short of a substring should -# we use? The goal is to be shorter than the full string, but -# long enough to still be globally unique. -use constant SUBSTR_SIZE => 20; -# However, for some fields, we use a different size. -use constant FIELD_SUBSTR_SIZE => { - alias => 11, - # Just the month and day. - deadline => -5, - creation_ts => -8, - delta_ts => -8, - percentage_complete => 1, - work_time => 3, - remaining_time => 3, - target_milestone => 15, - longdesc => 25, - # Just the hour and minute. - FIELD_TYPE_DATETIME, -5, -}; - -# For most fields, we add the length of the name of the field plus -# the SUBSTR_SIZE specified above to determine how large of a substring -# we're going to use. However, for some fields, it doesn't make sense to -# add in their field name this way. -use constant SUBSTR_NO_FIELD_ADD => FIELD_TYPE_DATETIME, qw( - target_milestone remaining_time percentage_complete work_time - attachments.mimetype attachments.submitter attachments.filename - attachments.description flagtypes.name -); - -################ -# Known Broken # -################ - -# See the KNOWN_BROKEN constant for a general description of these -# "_BROKEN" constants. - -# Shared between greaterthan and greaterthaneq. -# -# As with other fields, longdescs greaterthan matches if any comment -# matches (which might be OK). -# -# Same for keywords, and cc. Logically, all of these might -# be OK, but it makes the operation not the logical reverse of -# lessthaneq. What we're really saying here by marking these broken -# is that there ought to be some way of searching "all ccs" vs "any cc" -# (and same for the other fields). -use constant GREATERTHAN_BROKEN => ( - cc => { contains => [1] }, -); - -# allwords and allwordssubstr have these broken tests in common. -use constant ALLWORDS_BROKEN => ( - # allwordssubstr on cc fields matches against a single cc, - # instead of matching against all ccs on a bug. - cc => { contains => [1] }, - # bug 828344 changed how these searches operate to revert back to the 4.0 - # behavour, so these tests need to be updated (bug 849117). - 'flagtypes.name' => { contains => [1] }, - longdesc => { contains => [1] }, -); - -# Fields that don't generally work at all with changed* searches, but -# probably should. -use constant CHANGED_BROKEN => ( - classification => { contains => [1] }, - commenter => { contains => [1] }, - percentage_complete => { contains => [1] }, - 'requestees.login_name' => { contains => [1] }, - 'setters.login_name' => { contains => [1] }, - delta_ts => { contains => [1] }, -); - -# These are additional broken tests that changedfrom and changedto -# have in common. -use constant CHANGED_VALUE_BROKEN => ( - bug_group => { contains => [1] }, - cc => { contains => [1] }, - estimated_time => { contains => [1] }, - 'flagtypes.name' => { contains => [1] }, - keywords => { contains => [1] }, - 'longdescs.count' => { search => 1 }, - FIELD_TYPE_MULTI_SELECT, { contains => [1] }, -); - - -# Any test listed in KNOWN_BROKEN gets marked TODO by Test::More -# (using some complex code in Bugzilla::Test::Seach::FieldTest). -# This means that if you run the test under "prove -v", these tests will -# still show up as "not ok", but the test suite results won't show them -# as a failure. -# -# This constant contains operators as keys, which point to hashes. The hashes -# have field names as keys. Each field name points to a hash describing -# how that field/operator combination is broken. The "contains" -# array specifies that that particular "contains" test is expected -# to fail. If "search" is set to 1, then we expect the creation of the -# Bugzilla::Search object to fail. -# -# To allow handling custom fields, you can also use the field type as a key -# instead of the field name. Specifying explicit field names always overrides -# specifying a field type. -# -# Sometimes the operators have multiple tests, and one of them works -# while the other fails. In this case, we have a special override for -# "operator-value", which uniquely identifies tests. -use constant KNOWN_BROKEN => { - greaterthan => { GREATERTHAN_BROKEN }, - greaterthaneq => { GREATERTHAN_BROKEN }, - - 'allwordssubstr-<1>' => { ALLWORDS_BROKEN }, - 'allwords-<1>' => { - ALLWORDS_BROKEN, - }, - 'anywords-<1>' => { - 'flagtypes.name' => { contains => [1,2,3,4,5] }, - }, - 'anywords-<1> <2>' => { - 'flagtypes.name' => { contains => [3,4,5] }, - }, - 'anywordssubstr-<1> <2>' => { - 'flagtypes.name' => { contains => [3,4,5] }, - }, - - # setters.login_name and requestees.login name aren't tracked individually - # in bugs_activity, so can't be searched using this method. - # - # percentage_complete isn't tracked in bugs_activity (and it would be - # really hard to track). However, it adds a 0=0 term instead of using - # the changed* charts or simply denying them. - # - # delta_ts changedbefore/after should probably search for bugs based - # on their delta_ts. - # - # creation_ts changedbefore/after should search for bug creation dates. - # - # The commenter field changedbefore/after should search for comment - # creation dates. - # - # classification isn't being tracked properly in bugs_activity, I think. - # - # attach_data.thedata should search when attachments were created and - # who they were created by. - 'changedbefore' => { - CHANGED_BROKEN, - 'attach_data.thedata' => { contains => [1] }, - }, - 'changedafter' => { - 'attach_data.thedata' => { contains => [2,3,4] }, - classification => { contains => [2,3,4] }, - commenter => { contains => [2,3,4] }, - delta_ts => { contains => [2,3,4] }, - percentage_complete => { contains => [2,3,4] }, - 'requestees.login_name' => { contains => [2,3,4] }, - 'setters.login_name' => { contains => [2,3,4] }, - }, - changedfrom => { - CHANGED_BROKEN, - CHANGED_VALUE_BROKEN, - # All fields should have a way to search for "changing - # from a blank value" probably. - blocked => { contains => [3,4,5], no_criteria => 1 }, - dependson => { contains => [2,4,5], no_criteria => 1 }, - work_time => { contains => [1] }, - FIELD_TYPE_BUG_ID, { contains => [5], no_criteria => 1 }, - }, - # changeto doesn't find remaining_time changes (possibly due to us not - # tracking that data properly). - # - # multi-valued fields are stored as comma-separated strings, so you - # can't do changedfrom/to on them. - # - # Perhaps commenter can either tell you who the last commenter was, - # or if somebody commented at a given time (combined with other - # charts). - # - # longdesc changedto/from doesn't do anything; maybe it should. - # Same for attach_data.thedata. - changedto => { - CHANGED_BROKEN, - CHANGED_VALUE_BROKEN, - 'attach_data.thedata' => { contains => [1] }, - longdesc => { contains => [1] }, - remaining_time => { contains => [1] }, - }, - changedby => { - CHANGED_BROKEN, - # This should probably search the attacher or anybody who changed - # anything about an attachment at all. - 'attach_data.thedata' => { contains => [1] }, - # This should probably search the reporter. - creation_ts => { contains => [1] }, - }, - notequals => { - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - notregexp => { - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - notsubstring => { - 'flagtypes.name' => { contains => [5] }, - longdesc => { contains => [1] }, - }, - nowords => { - 'flagtypes.name' => { contains => [1, 5] }, - }, - nowordssubstr => { - 'flagtypes.name' => { contains => [5] }, - }, -}; - -################### -# Broken NotTests # -################### - -# Common BROKEN_NOT values for the changed* fields. -use constant CHANGED_BROKEN_NOT => ( - "attach_data.thedata" => { contains => [1] }, - "classification" => { contains => [1] }, - "commenter" => { contains => [1] }, - "delta_ts" => { contains => [1] }, - percentage_complete => { contains => [1] }, - "requestees.login_name" => { contains => [1] }, - "setters.login_name" => { contains => [1] }, -); - -# For changedfrom and changedto. -use constant CHANGED_FROM_TO_BROKEN_NOT => ( - 'longdescs.count' => { search => 1 }, - "bug_group" => { contains => [1] }, - "cc" => { contains => [1] }, - "estimated_time" => { contains => [1] }, - "flagtypes.name" => { contains => [1] }, - "keywords" => { contains => [1] }, - FIELD_TYPE_MULTI_SELECT, { contains => [1] }, -); - -# These are field/operator combinations that are broken when run under NOT(). -use constant BROKEN_NOT => { - allwords => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - 'allwords-<1> <2>' => { - cc => { }, - }, - allwordssubstr => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [5, 6] }, - longdesc => { contains => [1] }, - }, - 'allwordssubstr-<1>,<2>' => { - cc => { }, - longdesc => { contains => [1] }, - }, - anyexact => { - 'flagtypes.name' => { contains => [1, 2, 5] }, - }, - 'anywords-<1>' => { - 'flagtypes.name' => { contains => [1, 2, 3, 4, 5] }, - }, - 'anywords-<1> <2>' => { - 'flagtypes.name' => { contains => [3, 4, 5] }, - }, - anywordssubstr => { - 'flagtypes.name' => { contains => [5] }, - }, - 'anywordssubstr-<1> <2>' => { - 'flagtypes.name' => { contains => [3,4,5] }, - }, - casesubstring => { - 'flagtypes.name' => { contains => [5] }, - }, - changedafter => { - "attach_data.thedata" => { contains => [2, 3, 4] }, - "classification" => { contains => [2, 3, 4] }, - "commenter" => { contains => [2, 3, 4] }, - percentage_complete => { contains => [2, 3, 4] }, - "delta_ts" => { contains => [2, 3, 4] }, - "requestees.login_name" => { contains => [2, 3, 4] }, - "setters.login_name" => { contains => [2, 3, 4] }, - }, - changedbefore => { - CHANGED_BROKEN_NOT, - }, - changedby => { - CHANGED_BROKEN_NOT, - creation_ts => { contains => [1] }, - work_time => { contains => [1] }, - }, - changedfrom => { - CHANGED_BROKEN_NOT, - CHANGED_FROM_TO_BROKEN_NOT, - 'attach_data.thedata' => { }, - blocked => { contains => [1, 2] }, - dependson => { contains => [1, 3] }, - work_time => { contains => [1] }, - FIELD_TYPE_BUG_ID, { contains => [1 .. 4] }, - }, - changedto => { - CHANGED_BROKEN_NOT, - CHANGED_FROM_TO_BROKEN_NOT, - longdesc => { contains => [1] }, - "remaining_time" => { contains => [1] }, - }, - greaterthan => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [5] }, - }, - greaterthaneq => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [2, 5] }, - }, - equals => { - 'flagtypes.name' => { contains => [1, 5] }, - }, - notequals => { - longdesc => { contains => [1] }, - }, - notregexp => { - longdesc => { contains => [1] }, - }, - notsubstring => { - longdesc => { contains => [1] }, - }, - 'nowords-<1>' => { - 'flagtypes.name' => { contains => [5] }, - }, - 'nowordssubstr-<1>' => { - 'flagtypes.name' => { contains => [5] }, - }, - lessthan => { - 'flagtypes.name' => { contains => [5] }, - }, - lessthaneq => { - 'flagtypes.name' => { contains => [1, 5] }, - }, - regexp => { - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - substring => { - 'flagtypes.name' => { contains => [5] }, - longdesc => { contains => [1] }, - }, -}; - -############# -# Overrides # -############# - -# These overrides are used in the TESTS constant, below. - -# Regex tests need unique test values for certain fields. -use constant REGEX_OVERRIDE => { - 'attachments.mimetype' => { value => '^text/x-1-' }, - bug_file_loc => { value => '^http://1-' }, - see_also => { value => '^http://1-' }, - blocked => { value => '^<1>$' }, - dependson => { value => '^<1>$' }, - bug_id => { value => '^<1>$' }, - 'attachments.isobsolete' => { value => '^1'}, - 'attachments.ispatch' => { value => '^1'}, - 'attachments.isprivate' => { value => '^1' }, - cclist_accessible => { value => '^1' }, - reporter_accessible => { value => '^1' }, - everconfirmed => { value => '^1' }, - 'longdescs.count' => { value => '^3' }, - 'longdescs.isprivate' => { value => '^1' }, - creation_ts => { value => '^2037-01-01' }, - delta_ts => { value => '^2037-01-01' }, - deadline => { value => '^2037-02-01' }, - estimated_time => { value => '^1.0' }, - remaining_time => { value => '^9.0' }, - work_time => { value => '^1.0' }, - longdesc => { value => '^1-' }, - percentage_complete => { value => '^10' }, - FIELD_TYPE_BUG_ID, { value => '^<1>$' }, - FIELD_TYPE_DATETIME, { value => '^2037-03-01' } -}; - -# Common overrides between lessthan and lessthaneq. -use constant LESSTHAN_OVERRIDE => ( - alias => { contains => [1,5] }, - estimated_time => { contains => [1,5] }, - qa_contact => { contains => [1,5] }, - resolution => { contains => [1,5] }, - status_whiteboard => { contains => [1,5] }, - FIELD_TYPE_TEXTAREA, { contains => [1,5] }, - FIELD_TYPE_FREETEXT, { contains => [1,5] }, -); - -# The mandatorily-set fields have values higher than <1>, -# so bug 5 shows up. -use constant GREATERTHAN_OVERRIDE => ( - classification => { contains => [2,3,4,5] }, - assigned_to => { contains => [2,3,4,5] }, - bug_id => { contains => [2,3,4,5] }, - bug_group => { contains => [1,2,3,4] }, - bug_severity => { contains => [2,3,4,5] }, - bug_status => { contains => [2,3,4,5] }, - component => { contains => [2,3,4,5] }, - commenter => { contains => [2,3,4,5] }, - # keywords matches if *any* keyword matches - keywords => { contains => [1,2,3,4] }, - longdesc => { contains => [1,2,3,4] }, - op_sys => { contains => [2,3,4,5] }, - priority => { contains => [2,3,4,5] }, - product => { contains => [2,3,4,5] }, - reporter => { contains => [2,3,4,5] }, - rep_platform => { contains => [2,3,4,5] }, - short_desc => { contains => [2,3,4,5] }, - version => { contains => [2,3,4,5] }, - tag => { contains => [1,2,3,4] }, - target_milestone => { contains => [2,3,4,5] }, - # Bug 2 is the only bug besides 1 that has a Requestee set. - 'requestees.login_name' => { contains => [2] }, - FIELD_TYPE_SINGLE_SELECT, { contains => [2,3,4,5] }, - # Override SINGLE_SELECT for resolution. - resolution => { contains => [2,3,4] }, - # MULTI_SELECTs match if *any* value matches - FIELD_TYPE_MULTI_SELECT, { contains => [1,2,3,4] }, -); - -# For all positive multi-value types. -use constant MULTI_BOOLEAN_OVERRIDE => ( - 'attachments.ispatch' => { value => '1,1', contains => [1] }, - 'attachments.isobsolete' => { value => '1,1', contains => [1] }, - 'attachments.isprivate' => { value => '1,1', contains => [1] }, - cclist_accessible => { value => '1,1', contains => [1] }, - reporter_accessible => { value => '1,1', contains => [1] }, - 'longdescs.isprivate' => { value => '1,1', contains => [1] }, - everconfirmed => { value => '1,1', contains => [1] }, -); - -# Same as above, for negative multi-value types. -use constant NEGATIVE_MULTI_BOOLEAN_OVERRIDE => ( - 'attachments.ispatch' => { value => '1,1', contains => [2,3,4,5] }, - 'attachments.isobsolete' => { value => '1,1', contains => [2,3,4,5] }, - 'attachments.isprivate' => { value => '1,1', contains => [2,3,4,5] }, - cclist_accessible => { value => '1,1', contains => [2,3,4,5] }, - reporter_accessible => { value => '1,1', contains => [2,3,4,5] }, - 'longdescs.isprivate' => { value => '1,1', contains => [2,3,4,5] }, - everconfirmed => { value => '1,1', contains => [2,3,4,5] }, -); - -# For anyexact and anywordssubstr -use constant ANY_OVERRIDE => ( - 'longdescs.count' => { contains => [1,2,3,4] }, - 'work_time' => { value => '1.0,2.0' }, - dependson => { value => '<1>,<3>', contains => [1,3] }, - MULTI_BOOLEAN_OVERRIDE, -); - -# For all the changed* searches. The ones that have empty contains -# are fields that never change in value, or will never be rationally -# tracked in bugs_activity. -use constant CHANGED_OVERRIDE => ( - 'attachments.submitter' => { contains => [] }, - bug_id => { contains => [] }, - reporter => { contains => [] }, - tag => { contains => [] }, -); - -######### -# Tests # -######### - -# The basic format of this is a hashref, where the keys are operators, -# and each operator has an arrayref of tests that it runs. The tests -# are hashrefs, with the following possible keys: -# -# contains: This is a list of bug numbers that the search is expected -# to contain. (This is bug numbers, like 1,2,3, not the bug -# ids. For a description of each bug number, see NUM_BUGS.) -# Any bug not listed in "contains" must *not* show up in the -# search result. -# value: The value that you're searching for. There are certain special -# codes that will be replaced with bug values when the tests are -# run. In these examples below, "#" indicates a bug number: -# -# <#> - The field value for this bug. -# -# For any operator that has the string "word" in it, this is -# *all* the values for the current field from the numbered bug, -# joined by a space. -# -# If the operator has the string "substr" in it, then we -# take a substring of the value (for single-value searches) -# or we take a substring of each value and join them (for -# multi-value "word" searches). The length of the substring -# is determined by the SUBSTR_SIZE constants above.) -# -# For other operators, this just becomes the first value from -# the field for the numbered bug. -# -# So, if we were running the "equals" test and checking the -# cc field, <1> would become the login name of the first cc on -# Bug 1. If we did an "anywords" search test, it would become -# a space-separated string of the login names of all the ccs -# on Bug 1. If we did an "anywordssubstr" search test, it would -# become a space-separated string of the first few characters -# of each CC's login name on Bug 1. -# -# <#-id> - The bug id of the numbered bug. -# <#-reporter> - The login name of the numbered bug's reporter. -# <#-delta> - The delta_ts of the numbered bug. -# -# escape: If true, we will call quotemeta() on the value immediately -# before passing it to Search.pm. -# -# transform: A function to call on any field value before inserting -# it for a <#> replacement. The transformation function -# gets all of the bug's values for the field as its arguments. -# if_equal: This allows you to override "contains" for the case where -# the transformed value (from calling the "transform" function) -# is equal to the original value. -# -# override: This allows you to override "contains" and "values" for -# certain fields. -use constant TESTS => { - equals => [ - { contains => [1], value => '<1>' }, - ], - notequals => [ - { contains => [2,3,4,5], value => '<1>' }, - ], - substring => [ - { contains => [1], value => '<1>', - override => { - percentage_complete => { contains => [1,2,3] }, - } - }, - ], - casesubstring => [ - { contains => [1], value => '<1>', - override => { - percentage_complete => { contains => [1,2,3] }, - } - }, - { contains => [], value => '<1>', transform => sub { lc($_[0]) }, - extra_name => 'lc', if_equal => { contains => [1] }, - override => { - percentage_complete => { contains => [1,2,3] }, - } - }, - ], - notsubstring => [ - { contains => [2,3,4,5], value => '<1>', - override => { - percentage_complete => { contains => [4,5] }, - }, - } - ], - regexp => [ - { contains => [1], value => '<1>', escape => 1, - override => { - percentage_complete => { value => '^10' }, - } - }, - { contains => [1], value => '^1-', override => REGEX_OVERRIDE }, - ], - notregexp => [ - { contains => [2,3,4,5], value => '<1>', escape => 1, - override => { - percentage_complete => { value => '^10' }, - } - }, - { contains => [2,3,4,5], value => '^1-', override => REGEX_OVERRIDE }, - ], - lessthan => [ - { contains => [1], value => 2, - override => { - # A lot of these contain bug 5 because an empty value is validly - # less than the specified value. - bug_file_loc => { value => 'http://2-', contains => [1,5] }, - see_also => { value => 'http://2-' }, - 'attachments.mimetype' => { value => 'text/x-2-' }, - blocked => { value => '<4-id>', contains => [1,2] }, - dependson => { value => '<3-id>', contains => [1,3] }, - bug_id => { value => '<2-id>' }, - 'attachments.isprivate' => { value => 1, contains => [2,3,4] }, - 'attachments.isobsolete' => { value => 1, contains => [2,3,4] }, - 'attachments.ispatch' => { value => 1, contains => [2,3,4] }, - cclist_accessible => { value => 1, contains => [2,3,4,5] }, - reporter_accessible => { value => 1, contains => [2,3,4,5] }, - 'longdescs.count' => { value => 3, contains => [2,3,4,5] }, - 'longdescs.isprivate' => { value => 1, contains => [1,2,3,4,5] }, - everconfirmed => { value => 1, contains => [2,3,4,5] }, - creation_ts => { value => '2037-01-02', contains => [1,5] }, - delta_ts => { value => '2037-01-02', contains => [1,5] }, - deadline => { value => '2037-02-02', contains => [1,5] }, - remaining_time => { value => 10, contains => [1,5] }, - percentage_complete => { value => 11, contains => [1,5] }, - longdesc => { value => '2-', contains => [1,5] }, - work_time => { value => 1, contains => [5] }, - FIELD_TYPE_BUG_ID, { value => '<2>', contains => [1,5] }, - FIELD_TYPE_DATETIME, { value => '2037-03-02', contains => [1,5] }, - LESSTHAN_OVERRIDE, - } - }, - ], - lessthaneq => [ - { contains => [1], value => '<1>', - override => { - 'attachments.isobsolete' => { value => 0, contains => [2,3,4] }, - 'attachments.ispatch' => { value => 0, contains => [2,3,4] }, - 'attachments.isprivate' => { value => 0, contains => [2,3,4] }, - cclist_accessible => { value => 0, contains => [2,3,4,5] }, - reporter_accessible => { value => 0, contains => [2,3,4,5] }, - 'longdescs.count' => { value => 2, contains => [2,3,4,5] }, - 'longdescs.isprivate' => { value => -1, contains => [] }, - everconfirmed => { value => 0, contains => [2,3,4,5] }, - bug_file_loc => { contains => [1,5] }, - blocked => { contains => [1,2] }, - deadline => { contains => [1,5] }, - dependson => { contains => [1,3] }, - creation_ts => { contains => [1,5] }, - delta_ts => { contains => [1,5] }, - remaining_time => { contains => [1,5] }, - longdesc => { contains => [1,5] }, - percentage_complete => { contains => [1,5] }, - work_time => { value => 1, contains => [1,5] }, - FIELD_TYPE_BUG_ID, { contains => [1,5] }, - FIELD_TYPE_DATETIME, { contains => [1,5] }, - LESSTHAN_OVERRIDE, - }, - }, - ], - greaterthan => [ - { contains => [2,3,4], value => '<1>', - override => { - dependson => { contains => [3] }, - blocked => { contains => [2] }, - 'attachments.ispatch' => { value => 0, contains => [1] }, - 'attachments.isobsolete' => { value => 0, contains => [1] }, - 'attachments.isprivate' => { value => 0, contains => [1] }, - cclist_accessible => { value => 0, contains => [1] }, - reporter_accessible => { value => 0, contains => [1] }, - 'longdescs.count' => { value => 2, contains => [1] }, - 'longdescs.isprivate' => { value => 0, contains => [1] }, - everconfirmed => { value => 0, contains => [1] }, - 'flagtypes.name' => { value => 2, contains => [2,3,4] }, - GREATERTHAN_OVERRIDE, - }, - }, - ], - greaterthaneq => [ - { contains => [2,3,4], value => '<2>', - override => { - 'attachments.ispatch' => { value => 1, contains => [1] }, - 'attachments.isobsolete' => { value => 1, contains => [1] }, - 'attachments.isprivate' => { value => 1, contains => [1] }, - cclist_accessible => { value => 1, contains => [1] }, - reporter_accessible => { value => 1, contains => [1] }, - 'longdescs.count' => { value => 3, contains => [1] }, - 'longdescs.isprivate' => { value => 1, contains => [1] }, - everconfirmed => { value => 1, contains => [1] }, - dependson => { value => '<3>', contains => [1,3] }, - blocked => { contains => [1,2] }, - GREATERTHAN_OVERRIDE, - } - }, - ], - matches => [ - { contains => [1], value => '<1>' }, - ], - notmatches => [ - { contains => [2,3,4,5], value => '<1>' }, - ], - anyexact => [ - { contains => [1,2], value => '<1>, <2>', - override => { ANY_OVERRIDE } }, - ], - anywordssubstr => [ - { contains => [1,2], value => '<1> <2>', - override => { - ANY_OVERRIDE, - percentage_complete => { contains => [1,2,3] }, - } - }, - ], - allwordssubstr => [ - { contains => [1], value => '<1>', - override => { - MULTI_BOOLEAN_OVERRIDE, - # We search just the number "1" for percentage_complete, - # which matches a lot of bugs. - percentage_complete => { contains => [1,2,3] }, - }, - }, - { contains => [], value => '<1>,<2>', - override => { - dependson => { value => '<1-id> <3-id>', contains => [] }, - # bug 3 has the value "21" here, so matches "2,1" - percentage_complete => { value => '<2>,<3>', contains => [3] }, - # 1 0 matches bug 1, which has both public and private comments. - 'longdescs.isprivate' => { contains => [1] }, - } - }, - ], - nowordssubstr => [ - { contains => [2,3,4,5], value => '<1>', - override => { - # longdescs.isprivate translates to "1 0", so no bugs should - # show up. - 'longdescs.isprivate' => { contains => [] }, - percentage_complete => { contains => [4,5] }, - work_time => { contains => [2,3,4,5] }, - } - }, - ], - anywords => [ - { contains => [1], value => '<1>', - override => { - MULTI_BOOLEAN_OVERRIDE, - } - }, - { contains => [1,2], value => '<1> <2>', - override => { - MULTI_BOOLEAN_OVERRIDE, - dependson => { value => '<1> <3>', contains => [1,3] }, - 'longdescs.count' => { contains => [1,2,3,4] }, - }, - }, - ], - allwords => [ - { contains => [1], value => '<1>', - override => { MULTI_BOOLEAN_OVERRIDE } }, - { contains => [], value => '<1> <2>', - override => { - dependson => { contains => [], value => '<2-id> <3-id>' }, - # 1 0 matches bug 1, which has both public and private comments. - 'longdescs.isprivate' => { contains => [1] }, - } - }, - ], - nowords => [ - { contains => [2,3,4,5], value => '<1>', - override => { - # longdescs.isprivate translates to "1 0", so no bugs should - # show up. - 'longdescs.isprivate' => { contains => [] }, - work_time => { contains => [2,3,4,5] }, - } - }, - ], - - changedbefore => [ - { contains => [1], value => '<1-delta>', - override => { - CHANGED_OVERRIDE, - creation_ts => { contains => [1,5] }, - blocked => { contains => [1,2] }, - dependson => { contains => [1,3] }, - longdesc => { contains => [1,5] }, - 'longdescs.count' => { contains => [1,5] }, - } - }, - ], - changedafter => [ - { contains => [2,3,4], value => '<2-delta>', - override => { - CHANGED_OVERRIDE, - creation_ts => { contains => [3,4] }, - # We only change this for one bug, and it doesn't match. - 'longdescs.isprivate' => { contains => [] }, - # Same for everconfirmed. - 'everconfirmed' => { contains => [] }, - # For blocked and dependson, they have the delta_ts of bug1 - # in the bugs_activity table, so they won't ever match. - blocked => { contains => [] }, - dependson => { contains => [] }, - } - }, - ], - changedfrom => [ - { contains => [1], value => '<1>', - override => { - CHANGED_OVERRIDE, - # The test never changes an already-set dependency field, but - # we *can* attempt to test searching against an empty value, - # which should get us some bugs. - blocked => { value => '', contains => [1,2] }, - dependson => { value => '', contains => [1,3] }, - FIELD_TYPE_BUG_ID, { value => '', contains => [1,2,3,4] }, - # longdesc changedfrom doesn't make any sense. - longdesc => { contains => [] }, - # Nor does creation_ts changedfrom. - creation_ts => { contains => [] }, - 'attach_data.thedata' => { contains => [] }, - bug_id => { value => '<1-id>', contains => [] }, - }, - }, - ], - changedto => [ - { contains => [1], value => '<1>', - override => { - CHANGED_OVERRIDE, - # I can't imagine any use for creation_ts changedto. - creation_ts => { contains => [] }, - } - }, - ], - changedby => [ - { contains => [1], value => '<1-reporter>', - override => { - CHANGED_OVERRIDE, - blocked => { contains => [1,2] }, - dependson => { contains => [1,3] }, - }, - }, - ], - # XXX these need tests developed - isempty => [], - isnotempty => [], -}; - -# Fields that do not behave as we expect, for InjectionTest. -# search => 1 means the Bugzilla::Search creation fails. -# sql_error is a regex that specifies a SQL error that's OK for us to throw. -# operator_ok overrides the "brokenness" of certain operators, so that they -# are always OK for that field/operator combination. -use constant INJECTION_BROKEN_FIELD => { - # Pg can't run injection tests against integer or date fields. See bug 577557. - 'attachments.isobsolete' => { db_skip => ['Pg'] }, - 'attachments.ispatch' => { db_skip => ['Pg'] }, - 'attachments.isprivate' => { db_skip => ['Pg'] }, - blocked => { db_skip => ['Pg'] }, - bug_id => { db_skip => ['Pg'] }, - cclist_accessible => { db_skip => ['Pg'] }, - creation_ts => { db_skip => ['Pg'] }, - days_elapsed => { db_skip => ['Pg'] }, - dependson => { db_skip => ['Pg'] }, - deadline => { db_skip => ['Pg'] }, - delta_ts => { db_skip => ['Pg'] }, - estimated_time => { db_skip => ['Pg'] }, - everconfirmed => { db_skip => ['Pg'] }, - 'longdescs.isprivate' => { db_skip => ['Pg'] }, - percentage_complete => { db_skip => ['Pg'] }, - remaining_time => { db_skip => ['Pg'] }, - reporter_accessible => { db_skip => ['Pg'] }, - work_time => { db_skip => ['Pg'] }, - FIELD_TYPE_BUG_ID, { db_skip => ['Pg'] }, - FIELD_TYPE_DATETIME, { db_skip => ['Pg'] }, - owner_idle_time => { search => 1 }, - 'longdescs.count' => { - search => 1, - db_skip => ['Pg'], - operator_ok => [qw(allwords allwordssubstr anywordssubstr casesubstring - changedbefore changedafter greaterthan greaterthaneq - lessthan lessthaneq notregexp notsubstring - nowordssubstr regexp substring anywords - notequals nowords equals anyexact)], - }, -}; - -# Operators that do not behave as we expect, for InjectionTest. -# search => 1 means the Bugzilla::Search creation fails, but -# field_ok contains fields that it does actually succeed for. -use constant INJECTION_BROKEN_OPERATOR => { - changedafter => { search => 1, field_ok => ['creation_ts'] }, - changedbefore => { search => 1, field_ok => ['creation_ts'] }, - changedby => { search => 1 }, - isempty => { search => 1 }, - isnotempty => { search => 1 }, -}; - -# Tests run by Bugzilla::Test::Search::InjectionTest. -# We have to make sure the values are all one word or they'll be split -# up by the multi-word tests. -use constant INJECTION_TESTS => ( - { value => ';SEMICOLON_TEST' }, - { value => '--COMMENT_TEST' }, - { value => "'QUOTE_TEST" }, - { value => "';QUOTE_SEMICOLON_TEST" }, - { value => '/*STAR_COMMENT_TEST' } -); - -################# -# Special Tests # -################# - -use constant SPECIAL_PARAM_TESTS => ( - { field => 'bug_status', operator => 'anyexact', value => '__open__', - contains => [5] }, - { field => 'bug_status', operator => 'anyexact', value => '__closed__', - contains => [1,2,3,4] }, - { field => 'bug_status', operator => 'anyexact', value => '__all__', - contains => [1,2,3,4,5] }, - - { field => 'resolution', operator => 'anyexact', value => '---', - contains => [5] }, - - # email* query parameters. - { field => 'assigned_to', operator => 'anyexact', - value => '<1>, <2-reporter>', contains => [1,2], - extra_params => { emailreporter1 => 1 } }, - { field => 'assigned_to', operator => 'equals', - value => '<1>', extra_name => 'email2', contains => [], - extra_params => { - email2 => generate_random_password(100), emaillongdesc2 => 1, - }, - }, - - # standard pronouns - { field => 'assigned_to', operator => 'equals', value => '%assignee%', - contains => [1,2,3,4,5] }, - { field => 'reporter', operator => 'equals', value => '%reporter%', - contains => [1,2,3,4,5] }, - { field => 'qa_contact', operator => 'equals', value => '%qacontact%', - contains => [1,2,3,4,5] }, - { field => 'cc', operator => 'equals', value => '%user%', - contains => [1] }, - # group pronouns - { field => 'reporter', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] }, - { field => 'assigned_to', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] }, - { field => 'qa_contact', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4] }, - { field => 'cc', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4] }, - { field => 'commenter', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] }, -); - -use constant CUSTOM_SEARCH_TESTS => ( - { name => 'OP without CP', contains => [1], - params => [ - { f => 'OP' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - ] - }, - - { name => 'Empty OP/CP pair before criteria', contains => [1], - params => [ - { f => 'OP' }, { f => 'CP' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - ] - }, - - { name => 'Empty OP/CP pair after criteria', contains => [1], - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'OP' }, { f => 'CP' }, - ] - }, - - { name => 'empty OP/CP mid criteria', contains => [1], - columns => ['assigned_to'], - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'OP' }, { f => 'CP' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - ] - }, - - { name => 'bug_id = 1 AND assigned_to contains @', contains => [1], - columns => ['assigned_to'], - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - ] - }, - - { name => 'NOT(bug_id = 1) AND NOT(assigned_to = 2)', - contains => [3,4,5], - columns => ['assigned_to'], - params => [ - { n => 1, f => 'bug_id', o => 'equals', v => '<1>' }, - { n => 1, f => 'assigned_to', o => 'equals', v => '<2>' }, - ] - }, - - { name => 'bug_id = 1 OR assigned_to = 2', contains => [1,2], - columns => ['assigned_to'], top_params => { j_top => 'OR' }, - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - ] - }, - - { name => 'NOT(bug_id = 1 AND assigned_to = 1)', contains => [2,3,4,5], - columns => ['assigned_to'], - params => [ - { f => 'OP', n => 1 }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - ] - }, - - - { name => '(bug_id = 1 AND assigned_to contains @) ' - . ' OR (bug_id = 2 AND assigned_to contains @)', - contains => [1,2], columns => ['assigned_to'], - top_params => { j_top => 'OR' }, - params => [ - { f => 'OP' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - { f => 'CP' }, - { f => 'OP' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - { f => 'CP' }, - ] - }, - - { name => '(bug_id = 1 OR assigned_to = 2) ' - . ' AND (bug_id = 2 OR assigned_to = 1)', - contains => [1,2], columns => ['assigned_to'], - params => [ - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - { f => 'CP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - ] - }, - - { name => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) ' - . ' AND (bug_id = 2 OR assigned_to = 1) )', - contains => [1,2,3], columns => ['assigned_to'], - top_params => { j_top => 'OR' }, - params => [ - { f => 'bug_id', o => 'equals', v => '<3>' }, - { f => 'OP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - { f => 'CP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - { f => 'CP' }, - ] - }, - - { name => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) ' - . ' AND (bug_id = 2 OR assigned_to = 1) ) OR bug_id = 4', - contains => [1,2,3,4], columns => ['assigned_to'], - top_params => { j_top => 'OR' }, - params => [ - { f => 'bug_id', o => 'equals', v => '<3>' }, - { f => 'OP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - { f => 'CP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - { f => 'CP' }, - { f => 'bug_id', o => 'equals', v => '<4>' }, - ] - }, - -); - -1; diff --git a/xt/lib/Bugzilla/Test/Search/CustomTest.pm b/xt/lib/Bugzilla/Test/Search/CustomTest.pm deleted file mode 100644 index 132e5ac40..000000000 --- a/xt/lib/Bugzilla/Test/Search/CustomTest.pm +++ /dev/null @@ -1,101 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This module represents a test with custom URL parameters. -# Tests like this are specified in CUSTOM_SEARCH_TESTS in -# Bugzilla::Test::Search::Constants. -package Bugzilla::Test::Search::CustomTest; -use parent qw(Bugzilla::Test::Search::FieldTest); -use strict; -use warnings; - -use Bugzilla::Test::Search::FieldTest; -use Bugzilla::Test::Search::OperatorTest; - -use Storable qw(dclone); - -############### -# Constructor # -############### - -sub new { - my ($class, $test, $search_test) = @_; - bless { raw_test => dclone($test), search_test => $search_test }, $class; -} - -############# -# Accessors # -############# - -sub search_test { return $_[0]->{search_test} } -sub name { return 'Custom: ' . $_[0]->test->{name} } -sub test { return $_[0]->{raw_test} } - -sub operator_test { die "unimplemented" } -sub field_object { die "unimplemented" } -sub main_value { die "unimplenmented" } -sub test_value { die "unimplemented" } -# Custom tests don't use transforms. -sub transformed_value_was_equal { 0 } -sub debug_value { - my ($self) = @_; - my $string = ''; - my $params = $self->search_params; - foreach my $param (keys %$params) { - $string .= $param . "=" . $params->{$param} . '&'; - } - chop($string); - return $string; -} - -# The tests we know are broken for this operator/field combination. -sub _known_broken { return {} } -sub contains_known_broken { return undef } -sub search_known_broken { return undef } -sub field_not_yet_implemented { return undef } -sub invalid_field_operator_combination { return undef } - -######################################### -# Accessors: Bugzilla::Search Arguments # -######################################### - -# Converts the f, o, v rows into f0, o0, v0, etc. and translates -# the values appropriately. -sub search_params { - my ($self) = @_; - - my %params = %{ $self->test->{top_params} || {} }; - my $counter = 0; - foreach my $row (@{ $self->test->{params} }) { - $row->{v} = $self->translate_value($row) if exists $row->{v}; - foreach my $key (keys %$row) { - $params{"${key}$counter"} = $row->{$key}; - } - $counter++; - } - - return \%params; -} - -sub translate_value { - my ($self, $row) = @_; - my $as_test = { field => $row->{f}, operator => $row->{o}, - value => $row->{v} }; - my $operator_test = new Bugzilla::Test::Search::OperatorTest($row->{o}, - $self->search_test); - my $field = Bugzilla::Field->check($row->{f}); - my $field_test = new Bugzilla::Test::Search::FieldTest($operator_test, - $field, $as_test); - return $field_test->translated_value; -} - -sub search_columns { - my ($self) = @_; - return ['bug_id', @{ $self->test->{columns} || [] }]; -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/FieldTest.pm b/xt/lib/Bugzilla/Test/Search/FieldTest.pm deleted file mode 100644 index 5e86d92e2..000000000 --- a/xt/lib/Bugzilla/Test/Search/FieldTest.pm +++ /dev/null @@ -1,617 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This module represents the tests that get run on a single -# operator/field combination for Bugzilla::Test::Search. -# This is where all the actual testing happens. -package Bugzilla::Test::Search::FieldTest; - -use strict; -use warnings; -use Bugzilla::Search; -use Bugzilla::Test::Search::Constants; -use Bugzilla::Util qw(trim); - -use Data::Dumper; -use Scalar::Util qw(blessed); -use Test::More; -use Test::Exception; - -############### -# Constructor # -############### - -sub new { - my ($class, $operator_test, $field, $test) = @_; - return bless { operator_test => $operator_test, - field_object => $field, - raw_test => $test }, $class; -} - -############# -# Accessors # -############# - -sub num_tests { return TESTS_PER_RUN } - -# The Bugzilla::Test::Search::OperatorTest that this is a child of. -sub operator_test { return $_[0]->{operator_test} } -# The Bugzilla::Field being tested. -sub field_object { return $_[0]->{field_object} } -# The name of the field being tested, which we need much more often -# than we need the object. -sub field { - my ($self) = @_; - $self->{field_name} ||= $self->field_object->name; - return $self->{field_name}; -} -# The Bugzilla::Test::Search object that this is a child of. -sub search_test { return $_[0]->operator_test->search_test } -# The operator being tested -sub operator { return $_[0]->operator_test->operator } -# The bugs currently being tested by Bugzilla::Test::Search. -sub bugs { return $_[0]->search_test->bugs } -sub bug { - my $self = shift; - return $self->search_test->bug(@_); -} -sub number { - my ($self, $id) = @_; - foreach my $number (1..NUM_BUGS) { - return $number if $self->search_test->bug($number)->id == $id; - } - return 0; -} - -# The name displayed for this test by Test::More. Used in test descriptions. -sub name { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; - my $value = $self->main_value; - - my $name = "$field-$operator-$value"; - if (my $extra_name = $self->test->{extra_name}) { - $name .= "-$extra_name"; - } - return $name; -} - -# The appropriate value from the TESTS constant for this test, taking -# into account overrides. -sub test { - my $self = shift; - return $self->{test} if $self->{test}; - - my %test = %{ $self->{raw_test} }; - - # We have field name overrides... - my $override = $test{override}->{$self->field}; - # And also field type overrides. - if (!$override) { - $override = $test{override}->{$self->field_object->type} || {}; - } - - foreach my $key (%$override) { - $test{$key} = $override->{$key}; - } - - $self->{test} = \%test; - return $self->{test}; -} - -# All the values for all the bugs for this field. -sub _field_values { - my ($self) = @_; - return $self->{field_values} if $self->{field_values}; - - my %field_values; - foreach my $number (1..NUM_BUGS) { - $field_values{$number} = $self->_field_values_for_bug($number); - } - $self->{field_values} = \%field_values; - return $self->{field_values}; -} -# The values for this field for the numbered bug. -sub bug_values { - my ($self, $number) = @_; - return @{ $self->_field_values->{$number} }; -} - -# The untranslated, non-overriden value--used in the name of the test -# and other places. -sub main_value { return $_[0]->{raw_test}->{value} } -# The untranslated test value, taking into account overrides. -sub test_value { return $_[0]->test->{value} }; -# The value translated appropriately for passing to Bugzilla::Search. -sub translated_value { - my $self = shift; - if (!exists $self->{translated_value}) { - my $value = $self->search_test->value_translation_cache($self); - if (!defined $value) { - $value = $self->_translate_value(); - $self->search_test->value_translation_cache($self, $value); - } - $self->{translated_value} = $value; - } - return $self->{translated_value}; -} -# Used in failure diagnostic messages. -sub debug_fail { - my ($self, $number, $results, $sql) = @_; - my @expected = @{ $self->test->{contains} }; - my @results = sort - map { $self->number($_) } - map { $_->[0] } - @$results; - return - " Value: '" . $self->translated_value . "'\n" . - "Expected: [" . join(',', @expected) . "]\n" . - " Results: [" . join(',', @results) . "]\n" . - trim($sql) . "\n"; -} - -# True for a bug if we ran the "transform" function on it and the -# result was equal to its first value. -sub transformed_value_was_equal { - my ($self, $number, $value) = @_; - if (@_ > 2) { - $self->{transformed_value_was_equal}->{$number} = $value; - $self->search_test->was_equal_cache($self, $number, $value); - } - my $cached = $self->search_test->was_equal_cache($self, $number); - return $cached if defined $cached; - return $self->{transformed_value_was_equal}->{$number}; -} - -# True if this test is supposed to contain the numbered bug. -sub bug_is_contained { - my ($self, $number) = @_; - my $contains = $self->test->{contains}; - if ($self->transformed_value_was_equal($number) - and !$self->test->{override}->{$self->field}->{contains}) - { - $contains = $self->test->{if_equal}->{contains}; - } - return grep($_ == $number, @$contains) ? 1 : 0; -} - -################################################### -# Accessors: Ways of doing SKIP and TODO on tests # -################################################### - -# The tests we know are broken for this operator/field combination. -sub _known_broken { - my ($self, $constant, $skip_pg_check) = @_; - - $constant ||= KNOWN_BROKEN; - my $field = $self->field; - my $type = $self->field_object->type; - my $operator = $self->operator; - my $value = $self->main_value; - my $value_name = "$operator-$value"; - if (my $extra_name = $self->test->{extra_name}) { - $value_name .= "-$extra_name"; - } - - my $value_broken = $constant->{$value_name}->{$field}; - $value_broken ||= $constant->{$value_name}->{$type}; - return $value_broken if $value_broken; - my $operator_broken = $constant->{$operator}->{$field}; - $operator_broken ||= $constant->{$operator}->{$type}; - return $operator_broken if $operator_broken; - return {}; -} - -# True if the "contains" search for the numbered bug is broken. -# That is, either the result is supposed to contain it and doesn't, -# or the result is not supposed to contain it and does. -sub contains_known_broken { - my ($self, $number) = @_; - my $field = $self->field; - my $operator = $self->operator; - - my $contains_broken = $self->_known_broken->{contains} || []; - if (grep($_ == $number, @$contains_broken)) { - return "$field $operator contains $number is known to be broken"; - } - return undef; -} - -# Used by subclasses. Checks both bug_is_contained and contains_known_broken -# to tell you whether or not the bug will *actually* be found by the test. -sub will_actually_contain_bug { - my ($self, $number) = @_; - my $is_contained = $self->bug_is_contained($number) ? 1 : 0; - my $is_broken = $self->contains_known_broken($number) ? 1 : 0; - - # If the test is supposed to contain the bug and *isn't* broken, - # then the test will contain the bug. - return 1 if ($is_contained and !$is_broken); - # If this test is *not* supposed to contain the bug, but that test is - # broken, then this test *will* contain the bug. - return 1 if (!$is_contained and $is_broken); - - return 0; -} - -# Returns a string if creating a Bugzilla::Search object throws an error, -# with this field/operator/value combination. -sub search_known_broken { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; - if ($self->_known_broken->{search}) { - return "Bugzilla::Search for $field $operator is known to be broken"; - } - return undef; -} - -# Returns a string if we haven't yet implemented the tests for this field, -# but we plan to in the future. -sub field_not_yet_implemented { - my ($self) = @_; - my $skip_this_field = grep { $_ eq $self->field } SKIP_FIELDS; - if ($skip_this_field) { - my $field = $self->field; - return "$field testing not yet implemented"; - } - return undef; -} - -# Returns a message if this field/operator combination can't ever be run. -# At no time in the future will this field/operator combination ever work. -sub invalid_field_operator_combination { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; - - if ($field eq 'content' && $operator !~ /matches/) { - return "content field does not support $operator"; - } - elsif ($operator =~ /matches/ && $field ne 'content') { - return "matches operator does not support fields other than content"; - } - return undef; -} - -# True if this field is broken in an OR combination. -sub join_broken { - my ($self, $or_broken_map) = @_; - my $or_broken = $or_broken_map->{$self->field . '-' . $self->operator}; - if (!$or_broken) { - # See if this is a comment field, and in that case, if there's - # a generic entry for all comment fields. - my $is_comment_field = COMMENT_FIELDS->{$self->field}; - if ($is_comment_field) { - $or_broken = $or_broken_map->{'longdescs.-' . $self->operator}; - } - } - return $or_broken; -} - -######################################### -# Accessors: Bugzilla::Search Arguments # -######################################### - -# The data that will get passed to Bugzilla::Search as its arguments. -sub search_params { - my ($self) = @_; - return $self->{search_params} if $self->{search_params}; - - my %params = ( - "field0-0-0" => $self->field, - "type0-0-0" => $self->operator, - "value0-0-0" => $self->translated_value, - ); - - $self->{search_params} = \%params; - return $self->{search_params}; -} - -sub search_columns { - my ($self) = @_; - my $field = $self->field; - my @search_fields = qw(bug_id); - if ($self->field_object->buglist) { - my $col_name = COLUMN_TRANSLATION->{$field} || $field; - push(@search_fields, $col_name); - } - return \@search_fields; -} - - -################ -# Field Values # -################ - -sub _field_values_for_bug { - my ($self, $number) = @_; - my $field = $self->field; - - my @values; - - if ($field =~ /^attach.+\.(.+)$/ ) { - my $attach_field = $1; - $attach_field = ATTACHMENT_FIELDS->{$attach_field} || $attach_field; - @values = $self->_values_for($number, 'attachments', $attach_field); - } - elsif (my $flag_field = FLAG_FIELDS->{$field}) { - @values = $self->_values_for($number, 'flags', $flag_field); - } - elsif (my $translation = COMMENT_FIELDS->{$field}) { - @values = $self->_values_for($number, 'comments', $translation); - # We want the last value to come first, so that single-value - # searches use the last comment. - @values = reverse @values; - } - elsif ($field eq 'longdescs.count') { - @values = scalar(@{ $self->bug($number)->comments }); - } - elsif ($field eq 'work_time') { - @values = $self->_values_for($number, 'actual_time'); - } - elsif ($field eq 'bug_group') { - @values = $self->_values_for($number, 'groups_in', 'name'); - } - elsif ($field eq 'keywords') { - @values = $self->_values_for($number, 'keyword_objects', 'name'); - } - elsif ($field eq 'content') { - @values = $self->_values_for($number, 'short_desc'); - } - elsif ($field eq 'see_also') { - @values = $self->_values_for($number, 'see_also', 'name'); - } - elsif ($field eq 'tag') { - @values = $self->_values_for($number, 'tags'); - } - # Bugzilla::Bug truncates creation_ts, but we need the full value - # from the database. This has no special value for changedfrom, - # because it never changes. - elsif ($field eq 'creation_ts') { - my $bug = $self->bug($number); - my $creation_ts = Bugzilla->dbh->selectrow_array( - 'SELECT creation_ts FROM bugs WHERE bug_id = ?', - undef, $bug->id); - @values = ($creation_ts); - } - else { - @values = $self->_values_for($number, $field); - } - - # We convert user objects to their login name, here, all in one - # block for simplicity. - if (grep { $_ eq $field } USER_FIELDS) { - # requestees.login_name is empty for most bugs (but checking - # blessed(undef) handles that. - # Values that come from %original_values aren't User objects. - @values = map { blessed($_) ? $_->login : $_ } @values; - @values = grep { defined $_ } @values; - } - - return \@values; -} - -sub _values_for { - my ($self, $number, $bug_field, $item_field) = @_; - - my $item; - if ($self->operator eq 'changedfrom') { - $item = $self->search_test->bug_create_value($number, $bug_field); - } - else { - my $bug = $self->bug($number); - $item = $bug->$bug_field; - } - - if ($item_field) { - if ($bug_field eq 'flags' and $item_field eq 'name') { - return (map { $_->name . $_->status } @$item); - } - return (map { $self->_get_item($_, $item_field) } @$item); - } - - return @$item if ref($item) eq 'ARRAY'; - return $item if defined $item; - return (); -} - -sub _get_item { - my ($self, $from, $field) = @_; - if (blessed($from)) { - return $from->$field; - } - return $from->{$field}; -} - -##################### -# Value Translation # -##################### - -# This function translates the "value" specified in TESTS into an actual -# search value to pass to Search.pm. This means that we get the value -# from the current bug (or, in the case of changedfrom, from %original_values) -# and then we insert it as required into the "value" from TESTS. (For example, -# <1> becomes the value for the field from bug 1.) -sub _translate_value { - my $self = shift; - my $value = $self->test_value; - foreach my $number (1..NUM_BUGS) { - $value = $self->_translate_value_for_bug($number, $value); - } - # Sanity check to make sure that none of the <> stuff was left in. - if ($value =~ /<\d/) { - die $self->name . ": value untranslated: $value\n"; - } - return $value; -} - -sub _translate_value_for_bug { - my ($self, $number, $value) = @_; - - my $bug = $self->bug($number); - - my $bug_id = $bug->id; - $value =~ s/<$number-id>/$bug_id/g; - my $bug_delta = $bug->delta_ts; - $value =~ s/<$number-delta>/$bug_delta/g; - my $reporter = $bug->reporter->login; - $value =~ s/<$number-reporter>/$reporter/g; - if ($value =~ /<$number-bug_group>/) { - my @bug_groups = map { $_->name } @{ $bug->groups_in }; - @bug_groups = grep { $_ =~ /^\d+-group-/ } @bug_groups; - my $group = $bug_groups[0]; - $value =~ s/<$number-bug_group>/$group/g; - } - - my @bug_values = $self->bug_values($number); - return $value if !@bug_values; - - if ($self->operator =~ /substr/) { - @bug_values = map { $self->_substr_value($_) } @bug_values; - } - - my $string_value = $bug_values[0]; - if ($self->operator =~ /word/) { - $string_value = join(' ', @bug_values); - } - if (my $func = $self->test->{transform}) { - my $transformed = $func->(@bug_values); - my $is_equal = $transformed eq $bug_values[0] ? 1 : 0; - $self->transformed_value_was_equal($number, $is_equal); - $string_value = $transformed; - } - - if ($self->test->{escape}) { - $string_value = quotemeta($string_value); - } - $value =~ s/<$number>/$string_value/g; - - return $value; -} - -sub _substr_value { - my ($self, $value) = @_; - my $field = $self->field; - my $type = $self->field_object->type; - my $substr_size = SUBSTR_SIZE; - if (exists FIELD_SUBSTR_SIZE->{$field}) { - $substr_size = FIELD_SUBSTR_SIZE->{$field}; - } - elsif (exists FIELD_SUBSTR_SIZE->{$type}) { - $substr_size = FIELD_SUBSTR_SIZE->{$type}; - } - if ($substr_size > 0) { - # The field name is included in every field value, and if it's - # long, it might take up the whole substring, and we don't want that. - if (!grep { $_ eq $field or $_ eq $type } SUBSTR_NO_FIELD_ADD) { - $substr_size += length($field); - } - my $string = substr($value, 0, $substr_size); - return $string; - } - return substr($value, $substr_size); -} - -##################### -# Main Test Methods # -##################### - -sub run { - my ($self) = @_; - - my $invalid_combination = $self->invalid_field_operator_combination; - my $field_not_implemented = $self->field_not_yet_implemented; - - SKIP: { - skip($invalid_combination, $self->num_tests) if $invalid_combination; - TODO: { - todo_skip ($field_not_implemented, $self->num_tests) if $field_not_implemented; - $self->do_tests(); - } - } -} - -sub do_tests { - my ($self) = @_; - my $name = $self->name; - - my $search_broken = $self->search_known_broken; - - my $search = $self->_test_search_object_creation(); - - my $sql; - TODO: { - local $TODO = $search_broken if $search_broken; - lives_ok { $sql = $search->_sql } "$name: generate SQL"; - } - - my $results; - SKIP: { - skip "Can't run SQL without any SQL", 1 if !defined $sql; - $results = $self->_test_sql($search); - } - - $self->_test_content($results, $sql); -} - -sub _test_search_object_creation { - my ($self) = @_; - my $name = $self->name; - my @args = (fields => $self->search_columns, params => $self->search_params); - my $search; - lives_ok { $search = new Bugzilla::Search(@args) } - "$name: create search object"; - return $search; -} - -sub _test_sql { - my ($self, $search) = @_; - my $name = $self->name; - my $results; - lives_ok { $results = $search->data } "$name: Run SQL Query" - or diag($search->_sql); - return $results; -} - -sub _test_content { - my ($self, $results, $sql) = @_; - - SKIP: { - skip "Without results we can't test them", NUM_BUGS if !$results; - foreach my $number (1..NUM_BUGS) { - $self->_test_content_for_bug($number, $results, $sql); - } - } -} - -sub _test_content_for_bug { - my ($self, $number, $results, $sql) = @_; - my $name = $self->name; - - my $contains_known_broken = $self->contains_known_broken($number); - - my %result_ids = map { $_->[0] => 1 } @$results; - my $bug_id = $self->bug($number)->id; - - TODO: { - local $TODO = $contains_known_broken if $contains_known_broken; - if ($self->bug_is_contained($number)) { - ok($result_ids{$bug_id}, - "$name: contains bug $number ($bug_id)") - or diag $self->debug_fail($number, $results, $sql); - } - else { - ok(!$result_ids{$bug_id}, - "$name: does not contain bug $number ($bug_id)") - or diag $self->debug_fail($number, $results, $sql); - } - } -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm b/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm deleted file mode 100644 index 888e7eb13..000000000 --- a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm +++ /dev/null @@ -1,104 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This is the same as a FieldTest, except that it uses normal URL -# parameters instead of Boolean Charts. -package Bugzilla::Test::Search::FieldTestNormal; -use strict; -use warnings; -use parent qw(Bugzilla::Test::Search::FieldTest); - -use Scalar::Util qw(blessed); - -use constant CH_OPERATOR => { - changedafter => 'chfieldfrom', - changedbefore => 'chfieldto', - changedto => 'chfieldvalue', -}; - -use constant EMAIL_FIELDS => qw(assigned_to qa_contact cc reporter commenter); - -# Normally, we just clone a FieldTest because that's the best for performance, -# overall--that way we don't have to translate the value again. However, -# sometimes (like in Bugzilla::Test::Search's direct code) we just want -# to create a FieldTestNormal. -sub new { - my $class = shift; - my ($first_arg) = @_; - if (blessed $first_arg - and $first_arg->isa('Bugzilla::Test::Search::FieldTest')) - { - my $self = { %$first_arg }; - return bless $self, $class; - } - return $class->SUPER::new(@_); -} - -sub name { - my $self = shift; - my $name = $self->SUPER::name(@_); - return "$name (Normal Params)"; -} - -sub search_columns { - my $self = shift; - my $field = $self->field; - # For the assigned_to, qa_contact, and reporter fields, have the - # "Normal Params" test check that the _realname columns work - # all by themselves. - if (grep($_ eq $field, EMAIL_FIELDS) && $self->field_object->buglist) { - return ['bug_id', "${field}_realname"] - } - return $self->SUPER::search_columns(@_); -} - -sub search_params { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; - my $value = $self->translated_value; - if ($operator eq 'anyexact') { - $value = [split ',', $value]; - } - - if (my $ch_param = CH_OPERATOR->{$operator}) { - if ($field eq 'creation_ts') { - $field = '[Bug creation]'; - } - return { chfield => $field, $ch_param => $value }; - } - - if ($field eq 'delta_ts' and $operator eq 'greaterthaneq') { - return { chfieldfrom => $value }; - } - if ($field eq 'delta_ts' and $operator eq 'lessthaneq') { - return { chfieldto => $value }; - } - - if ($field eq 'deadline' and $operator eq 'greaterthaneq') { - return { deadlinefrom => $value }; - } - if ($field eq 'deadline' and $operator eq 'lessthaneq') { - return { deadlineto => $value }; - } - - if (grep { $_ eq $field } EMAIL_FIELDS) { - $field = 'longdesc' if $field eq 'commenter'; - return { - email1 => $value, - "email${field}1" => 1, - emailtype1 => $operator, - # Used to do extra tests on special sorts of email* combinations. - %{ $self->test->{extra_params} || {} }, - }; - } - - $field =~ s/\./_/g; - return { $field => $value, "${field}_type" => $operator }; -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm b/xt/lib/Bugzilla/Test/Search/InjectionTest.pm deleted file mode 100644 index 90eaabc78..000000000 --- a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm +++ /dev/null @@ -1,77 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This module represents the SQL Injection tests that get run on a single -# operator/field combination for Bugzilla::Test::Search. -package Bugzilla::Test::Search::InjectionTest; -use parent qw(Bugzilla::Test::Search::FieldTest); - -use strict; -use warnings; -use Bugzilla::Test::Search::Constants; -use Test::Exception; - -sub num_tests { return NUM_SEARCH_TESTS } - -sub _known_broken { - my ($self) = @_; - my $operator_broken = INJECTION_BROKEN_OPERATOR->{$self->operator}; - # We don't want to auto-vivify $operator_broken and thus make it true. - my @field_ok = $operator_broken ? @{ $operator_broken->{field_ok} || [] } - : (); - $operator_broken = undef if grep { $_ eq $self->field } @field_ok; - - my $field_broken = INJECTION_BROKEN_FIELD->{$self->field} - || INJECTION_BROKEN_FIELD->{$self->field_object->type}; - # We don't want to auto-vivify $field_broken and thus make it true. - my @operator_ok = $field_broken ? @{ $field_broken->{operator_ok} || [] } - : (); - $field_broken = undef if grep { $_ eq $self->operator } @operator_ok; - - return $operator_broken || $field_broken || {}; -} - -sub sql_error_ok { return $_[0]->_known_broken->{sql_error} } - -# Injection tests only skip fields on certain dbs. -sub field_not_yet_implemented { - my ($self) = @_; - # We use the constant directly because we don't want operator_ok - # or field_ok to stop us. - my $broken = INJECTION_BROKEN_FIELD->{$self->field} - || INJECTION_BROKEN_FIELD->{$self->field_object->type}; - my $skip_for_dbs = $broken->{db_skip}; - return undef if !$skip_for_dbs; - my $dbh = Bugzilla->dbh; - if (my ($skip) = grep { $dbh->isa("Bugzilla::DB::$_") } @$skip_for_dbs) { - my $field = $self->field; - return "$field injection testing is not supported with $skip"; - } - return undef; -} -# Injection tests don't do translation. -sub translated_value { $_[0]->test_value } - -sub name { return "injection-" . $_[0]->SUPER::name; } - -# Injection tests don't check content. -sub _test_content {} - -sub _test_sql { - my $self = shift; - my ($sql) = @_; - my $dbh = Bugzilla->dbh; - my $name = $self->name; - if (my $error_ok = $self->sql_error_ok) { - throws_ok { $dbh->selectall_arrayref($sql) } $error_ok, - "$name: SQL query dies, as we expect"; - return; - } - return $self->SUPER::_test_sql(@_); -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/NotTest.pm b/xt/lib/Bugzilla/Test/Search/NotTest.pm deleted file mode 100644 index 190b8567b..000000000 --- a/xt/lib/Bugzilla/Test/Search/NotTest.pm +++ /dev/null @@ -1,61 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This module runs tests just like a normal FieldTest, AndTest, -# or OrTest, but in a NOT chart instead of a normal chart. -# -# Logically this should be a mixin of some sort so that we can apply -# it to OrTest and AndTest, but without Moose there isn't much of an -# easy way to do that. -package Bugzilla::Test::Search::NotTest; -use parent qw(Bugzilla::Test::Search::FieldTest); -use strict; -use warnings; -use Bugzilla::Test::Search::Constants; - -# We just clone a FieldTest because that's the best for performance, -# overall--that way we don't have to translate the value again. -sub new { - my ($class, $field_test) = @_; - my $self = { %$field_test }; - return bless $self, $class; -} - -############# -# Accessors # -############# - -sub name { - my ($self) = @_; - return "NOT(" . $self->SUPER::name . ")"; -} - -# True if this test is supposed to contain the numbered bug. Reversed for -# NOT tests. -sub bug_is_contained { - my $self = shift; - my ($number) = @_; - # No search ever returns bug 6, because it's protected by security groups - # that the searcher isn't a member of. - return 0 if $number == 6; - return $self->SUPER::bug_is_contained(@_) ? 0 : 1; -} - -# NOT tests have their own constant for tracking broken-ness. -sub _known_broken { - my ($self) = @_; - return $self->SUPER::_known_broken(BROKEN_NOT, 'skip pg check'); -} - -sub search_params { - my ($self) = @_; - my %params = %{ $self->SUPER::search_params() }; - $params{negate0} = 1; - return \%params; -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/OperatorTest.pm b/xt/lib/Bugzilla/Test/Search/OperatorTest.pm deleted file mode 100644 index 5ab502dfc..000000000 --- a/xt/lib/Bugzilla/Test/Search/OperatorTest.pm +++ /dev/null @@ -1,103 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This module represents the tests that get run on a single operator -# from the TESTS constant in Bugzilla::Search::Test::Constants. -package Bugzilla::Test::Search::OperatorTest; - -use strict; -use warnings; -use Bugzilla::Test::Search::Constants; -use Bugzilla::Test::Search::FieldTest; -use Bugzilla::Test::Search::FieldTestNormal; -use Bugzilla::Test::Search::InjectionTest; -use Bugzilla::Test::Search::OrTest; -use Bugzilla::Test::Search::AndTest; -use Bugzilla::Test::Search::NotTest; - -############### -# Constructor # -############### - -sub new { - my ($invocant, $operator, $search_test) = @_; - $search_test ||= $invocant->search_test; - my $class = ref($invocant) || $invocant; - return bless { search_test => $search_test, operator => $operator }, $class; -} - -############# -# Accessors # -############# - -# The Bugzilla::Test::Search object that this is a child of. -sub search_test { return $_[0]->{search_test} } -# The operator being tested -sub operator { return $_[0]->{operator} } -# The tests that we're going to run on this operator. -sub tests { return @{ TESTS->{$_[0]->operator } } } -# The fields we're going to test for this operator. -sub test_fields { return $_[0]->search_test->all_fields } - -sub run { - my ($self) = @_; - - foreach my $field ($self->test_fields) { - foreach my $test ($self->tests) { - my $field_test = - new Bugzilla::Test::Search::FieldTest($self, $field, $test); - $field_test->run(); - my $normal_test = - new Bugzilla::Test::Search::FieldTestNormal($field_test); - $normal_test->run(); - my $not_test = new Bugzilla::Test::Search::NotTest($field_test); - $not_test->run(); - - next if !$self->search_test->option('long'); - - # Run the OR tests. This tests every other operator (including - # this operator itself) in combination with every other field, - # in an OR with this operator and field. - foreach my $other_operator ($self->search_test->all_operators) { - $self->run_join_tests($field_test, $other_operator); - } - } - foreach my $test (INJECTION_TESTS) { - my $injection_test = - new Bugzilla::Test::Search::InjectionTest($self, $field, $test); - $injection_test->run(); - } - } -} - -sub run_join_tests { - my ($self, $field_test, $other_operator) = @_; - - my $other_operator_test = $self->new($other_operator); - foreach my $other_test ($other_operator_test->tests) { - foreach my $other_field ($self->test_fields) { - $self->_run_one_join_test($field_test, $other_operator_test, - $other_field, $other_test); - $self->search_test->clean_test_history(); - } - } -} - -sub _run_one_join_test { - my ($self, $field_test, $other_operator_test, $other_field, $other_test) = @_; - my $other_field_test = - new Bugzilla::Test::Search::FieldTest($other_operator_test, - $other_field, $other_test); - my $or_test = new Bugzilla::Test::Search::OrTest($field_test, - $other_field_test); - $or_test->run(); - my $and_test = new Bugzilla::Test::Search::AndTest($field_test, - $other_field_test); - $and_test->run(); -} - -1; diff --git a/xt/lib/Bugzilla/Test/Search/OrTest.pm b/xt/lib/Bugzilla/Test/Search/OrTest.pm deleted file mode 100644 index 1b948f38d..000000000 --- a/xt/lib/Bugzilla/Test/Search/OrTest.pm +++ /dev/null @@ -1,141 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# This test combines two field/operator combinations using OR in -# a single boolean chart. -package Bugzilla::Test::Search::OrTest; -use parent qw(Bugzilla::Test::Search::FieldTest); - -use Bugzilla::Test::Search::Constants; -use List::MoreUtils qw(all any uniq); - -use constant type => 'OR'; - -############### -# Constructor # -############### - -sub new { - my $class = shift; - my $self = { field_tests => [@_] }; - return bless $self, $class; -} - -############# -# Accessors # -############# - -sub field_tests { return @{ $_[0]->{field_tests} } } -sub search_test { ($_[0]->field_tests)[0]->search_test } - -sub name { - my ($self) = @_; - my @names = map { $_->name } $self->field_tests; - return join('-' . $self->type . '-', @names); -} - -# In an OR test, bugs ARE supposed to be contained if they are contained -# by ANY test. -sub bug_is_contained { - my ($self, $number) = @_; - return any { $_->bug_is_contained($number) } $self->field_tests; -} - -# Needed only for failure messages -sub debug_value { - my ($self) = @_; - my @values = map { $_->field . ' ' . $_->debug_value } $self->field_tests; - return join(' ' . $self->type . ' ', @values); -} - -######################## -# SKIP & TODO Messages # -######################## - -sub field_not_yet_implemented { - my ($self) = @_; - return $self->_join_messages('field_not_yet_implemented'); -} -sub invalid_field_operator_combination { - my ($self) = @_; - return $self->_join_messages('invalid_field_operator_combination'); -} -sub search_known_broken { - my ($self) = @_; - return $self->_join_messages('search_known_broken'); -} - -sub _join_messages { - my ($self, $message_method) = @_; - my @messages = map { $_->$message_method } $self->field_tests; - @messages = grep { $_ } @messages; - return join(' AND ', @messages); -} - -sub _bug_will_actually_be_contained { - my ($self, $number) = @_; - - foreach my $test ($self->field_tests) { - # Some tests are broken in such a way that they actually - # generate no criteria in the SQL. In this case, the only way - # the test contains the bug is if *another* test contains it. - next if $test->_known_broken->{no_criteria}; - return 1 if $test->will_actually_contain_bug($number); - } - return 0; -} - -sub contains_known_broken { - my ($self, $number) = @_; - - if ( ( $self->bug_is_contained($number) - and !$self->_bug_will_actually_be_contained($number) ) - or ( !$self->bug_is_contained($number) - and $self->_bug_will_actually_be_contained($number) ) ) - { - my @messages = map { $_->contains_known_broken($number) } - $self->field_tests; - @messages = grep { $_ } @messages; - # Sometimes, with things that break because of no_criteria, there won't - # be anything in @messages even though we need to print out a message. - if (!@messages) { - my @no_criteria = grep { $_->_known_broken->{no_criteria} } - $self->field_tests; - @messages = map { "No criteria generated by " . $_->name } - @no_criteria; - } - die "broken test with no message" if !@messages; - return join(' AND ', @messages); - } - return undef; -} - -############################## -# Bugzilla::Search arguments # -############################## - -sub search_columns { - my ($self) = @_; - my @columns = map { @{ $_->search_columns } } $self->field_tests; - return [uniq @columns]; -} - -sub search_params { - my ($self) = @_; - my @all_params = map { $_->search_params } $self->field_tests; - my %params; - my $chart = 0; - foreach my $item (@all_params) { - $params{"field0-0-$chart"} = $item->{'field0-0-0'}; - $params{"type0-0-$chart"} = $item->{'type0-0-0'}; - $params{"value0-0-$chart"} = $item->{'value0-0-0'}; - $chart++; - } - return \%params; -} - -1; diff --git a/xt/lib/QA/REST.pm b/xt/lib/QA/REST.pm new file mode 100644 index 000000000..4de985668 --- /dev/null +++ b/xt/lib/QA/REST.pm @@ -0,0 +1,65 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package QA::REST; + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5"; + +use autodie; + +use LWP::UserAgent; +use JSON; +use QA::Util; + +use parent qw(LWP::UserAgent Exporter); + +@QA::REST::EXPORT = qw( + MUST_FAIL + get_rest_client +); + +use constant MUST_FAIL => 1; + +sub get_rest_client { + my $rest_client = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } ); + bless($rest_client, 'QA::REST'); + my $config = $rest_client->{bz_config} = get_config(); + $rest_client->{bz_url} = $config->{browser_url} . '/' . $config->{bugzilla_installation} . '/rest/'; + $rest_client->{bz_default_headers} = {'Accept' => 'application/json', 'Content-Type' => 'application/json'}; + return $rest_client; +} + +sub bz_config { return $_[0]->{bz_config}; } + +sub call { + my ($self, $method, $data, $http_verb, $expect_to_fail) = @_; + $http_verb = lc($http_verb || 'GET'); + $data //= {}; + + my %args = %{ $self->{bz_default_headers} }; + # We do not pass the API key in the URL, so that it's not logged by the web server. + if ($http_verb eq 'get' && $data->{api_key}) { + $args{'X-BUGZILLA-API-KEY'} = $data->{api_key}; + } + elsif ($http_verb ne 'get') { + $args{Content} = encode_json($data); + } + + my $response = $self->$http_verb($self->{bz_url} . $method, %args); + my $res = decode_json($response->decoded_content); + if ($response->is_success xor $expect_to_fail) { + return $res; + } + else { + die 'error ' . $res->{code} . ': ' . $res->{message} . "\n"; + } +} diff --git a/xt/lib/QA/RPC.pm b/xt/lib/QA/RPC.pm new file mode 100644 index 000000000..63a7d9503 --- /dev/null +++ b/xt/lib/QA/RPC.pm @@ -0,0 +1,289 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +# -*- Mode: perl; indent-tabs-mode: nil -*- + +package QA::RPC; + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5"; + +use Data::Dumper; +use QA::Util; +use QA::Tests qw(PRIVATE_BUG_USER create_bug_fields); +use Storable qw(dclone); +use Test::More; + +sub bz_config { + my $self = shift; + $self->{bz_config} ||= QA::Util::get_config(); + return $self->{bz_config}; +} + +# True if we're doing calls over GET instead of POST. +sub bz_get_mode { return 0 } + +# When doing bz_log_in over GET, we can't actually call User.login, +# we just store credentials here and then pass them as Bugzilla_login +# and Bugzilla_password with every future call until User.logout is called +# (which actually just calls _bz_clear_credentials, under GET). +sub _bz_credentials { + my ($self, $user, $pass) = @_; + if (@_ == 3) { + $self->{_bz_credentials}->{user} = $user; + $self->{_bz_credentials}->{pass} = $pass; + } + return $self->{_bz_credentials}; +} +sub _bz_clear_credentials { delete $_[0]->{_bz_credentials} } + +################################ +# Helpers for RPC test scripts # +################################ + +sub bz_log_in { + my ($self, $user) = @_; + my $username = $self->bz_config->{"${user}_user_login"}; + my $password = $self->bz_config->{"${user}_user_passwd"}; + + if ($self->bz_get_mode) { + $self->_bz_credentials($username, $password); + return; + } + + my $call = $self->bz_call_success( + 'User.login', { login => $username, password => $password }); + cmp_ok($call->result->{id}, 'gt', 0, $self->TYPE . ": Logged in as $user"); + $self->{_bz_credentials}->{token} = $call->result->{token}; +} + +sub bz_call_success { + my ($self, $method, $orig_args, $test_name) = @_; + my $args = $orig_args ? dclone($orig_args) : {}; + + if ($self->bz_get_mode and $method eq 'User.logout') { + $self->_bz_clear_credentials(); + return; + } + + my $call; + # Under XMLRPC::Lite, if we pass undef as the second argument, + # it sends a single param , 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 .= "¶ms=$params_escaped"; + } + if ($self->version eq '1.1') { + $url .= "&version=1.1"; + } + my $callback = delete $args->{callback}; + if (defined $callback) { + $self->_bz_callback($callback); + $url .= "&callback=" . uri_escape($callback); + } + $result = $self->SUPER::call($url); + } + else { + $result = $self->SUPER::call($url, \%params); + } + + if ($result) { + bless $result, 'QA::RPC::JSONRPC::ReturnObject'; + } + return $result; +} + +sub _get { + my $self = shift; + my $result = $self->SUPER::_get(@_); + # Simple JSONP support for tests. We just remove the callback from + # the return value. + my $callback = $self->_bz_callback; + if (defined $callback and $result->is_success) { + my $content = $result->content; + $content =~ s/^(?:\/\*\*\/)?\Q$callback(\E(.*)\)$/$1/s; + $result->content($content); + # We don't need this anymore, and we don't want it to affect + # future calls. + delete $self->{bz_callback}; + } + return $result; +} + +1; + +package QA::RPC::JSONRPC::ReturnObject; +use strict; + +BEGIN { + if (eval { require JSON::RPC::Client }) { + our @ISA = qw(JSON::RPC::ReturnObject); + } + else { + require JSON::RPC::Legacy::Client; + our @ISA = qw(JSON::RPC::Legacy::ReturnObject); + } +} + +################################# +# Consistency with XMLRPC::Lite # +################################# + +sub faultstring { $_[0]->{content}->{error}->{message} } +sub faultcode { $_[0]->{content}->{error}->{code} } +sub fault { $_[0]->is_error } + +1; + +package QA::RPC::UserAgent; +use strict; +use base qw(LWP::UserAgent); + +######################################## +# Consistency with XMLRPC::Lite's ->ua # +######################################## + +sub send_request { + my $self = shift; + my $response = $self->SUPER::send_request(@_); + $self->http_response($response); + # JSON::RPC::Client can't handle 500 responses, even though + # they're required by the JSON-RPC spec. + $response->code(200); + return $response; +} + +# Copied directly from SOAP::Lite::Transport::HTTP. +sub http_response { + my $self = shift; + if (@_) { $self->{'_http_response'} = shift; return $self } + return $self->{'_http_response'}; +} diff --git a/xt/lib/QA/RPC/XMLRPC.pm b/xt/lib/QA/RPC/XMLRPC.pm new file mode 100644 index 000000000..cb227fa9c --- /dev/null +++ b/xt/lib/QA/RPC/XMLRPC.pm @@ -0,0 +1,26 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +# -*- Mode: perl; indent-tabs-mode: nil -*- + +package QA::RPC::XMLRPC; + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../../../lib", "$RealBin/../../../../local/lib/perl5"; + +use base qw(QA::RPC XMLRPC::Lite); + +use constant TYPE => 'XML-RPC'; +use constant DATETIME_REGEX => qr/^\d{8}T\d\d:\d\d:\d\d$/; + +1; + +__END__ diff --git a/xt/lib/QA/Tests.pm b/xt/lib/QA/Tests.pm new file mode 100644 index 000000000..fe5f2d067 --- /dev/null +++ b/xt/lib/QA/Tests.pm @@ -0,0 +1,115 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +# -*- Mode: perl; indent-tabs-mode: nil -*- + +package QA::Tests; + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5"; + +use base qw(Exporter); +our @EXPORT_OK = qw( + PRIVATE_BUG_USER + STANDARD_BUG_TESTS + bug_tests + create_bug_fields +); + +use constant INVALID_BUG_ID => -1; +use constant INVALID_BUG_ALIAS => 'aaaaaaa12345'; +use constant PRIVATE_BUG_USER => 'QA_Selenium_TEST'; + +use constant CREATE_BUG => { + 'priority' => 'Highest', + 'status' => 'CONFIRMED', + 'version' => 'unspecified', + 'creator' => 'editbugs', + 'description' => '-- Comment Created By Bugzilla XML-RPC Tests --', + 'cc' => ['unprivileged'], + 'component' => 'c1', + 'platform' => 'PC', + # It's necessary to assign the bug to somebody who isn't in the + # timetracking group, for the Bug.update tests. + 'assigned_to' => PRIVATE_BUG_USER, + 'summary' => 'WebService Test Bug', + 'product' => 'Another Product', + 'op_sys' => 'Linux', + 'severity' => 'normal', + 'qa_contact' => 'canconfirm', + version => 'Another1', + url => 'http://www.bugzilla.org/', + target_milestone => 'AnotherMS1', +}; + +sub create_bug_fields { + my ($config) = @_; + my %bug = %{ CREATE_BUG() }; + foreach my $field (qw(creator assigned_to qa_contact)) { + my $value = $bug{$field}; + $bug{$field} = $config->{"${value}_user_login"}; + } + $bug{cc} = [map { $config->{$_ . "_user_login"} } @{ $bug{cc} }]; + return \%bug; +} + +sub bug_tests { + my ($public_id, $private_id) = @_; + return [ + { args => { ids => [$private_id] }, + error => "You are not authorized to access", + test => 'Logged-out user cannot access a private bug', + }, + { args => { ids => [$public_id] }, + test => 'Logged-out user can access a public bug.', + }, + { args => { ids => [INVALID_BUG_ID] }, + error => "not a valid bug number", + test => 'Passing invalid bug id returns error "Invalid Bug ID"', + }, + { args => { ids => [undef] }, + error => "You must enter a valid bug number", + test => 'Passing undef as bug id param returns error "Invalid Bug ID"', + }, + { args => { ids => [INVALID_BUG_ALIAS] }, + error => "nor an alias to a bug", + test => 'Passing invalid bug alias returns error "Invalid Bug Alias"', + }, + + { user => 'editbugs', + args => { ids => [$private_id] }, + error => "You are not authorized to access", + test => 'Access to a private bug is denied to a user without privs', + }, + { user => 'unprivileged', + args => { ids => [$public_id] }, + test => 'User without privs can access a public bug', + }, + { user => 'admin', + args => { ids => [$public_id] }, + test => 'Admin can access a public bug.', + }, + { user => PRIVATE_BUG_USER, + args => { ids => [$private_id] }, + test => 'User with privs can successfully access a private bug', + }, + # This helps webservice_bug_attachment get private attachment ids + # from the public bug, and doesn't hurt for the other tests. + { user => PRIVATE_BUG_USER, + args => { ids => [$public_id] }, + test => 'User with privs can also access the public bug', + }, + ]; +} + +use constant STANDARD_BUG_TESTS => bug_tests('public_bug', 'private_bug'); + +1; diff --git a/xt/lib/QA/Util.pm b/xt/lib/QA/Util.pm new file mode 100644 index 000000000..e122e41db --- /dev/null +++ b/xt/lib/QA/Util.pm @@ -0,0 +1,372 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +# -*- Mode: perl; indent-tabs-mode: nil -*- + +package QA::Util; + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../../lib", "$RealBin/../../../local/lib/perl5"; + +use autodie; +use Data::Dumper; +use Test::More; + +use parent qw(Exporter); +@QA::Util::EXPORT = qw( + trim + url_quote + random_string + + log_in + logout + file_bug_in_product + create_bug + edit_bug + edit_bug_and_return + go_to_bug + go_to_home + go_to_admin + edit_product + add_product + open_advanced_search_page + set_parameters + + get_selenium + get_rpc_clients + get_config + + WAIT_TIME + CHROME_MODE +); + +# How long we wait for pages to load. +use constant WAIT_TIME => 60000; +use constant CONF_FILE => "$RealBin/../config/selenium_test.conf"; +use constant CHROME_MODE => 1; +use constant NDASH => chr(0x2013); + +##################### +# Utility Functions # +##################### + +sub random_string { + my $size = shift || 30; # default to 30 chars if nothing specified + return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size)); +} + +# Remove consecutive as well as leading and trailing whitespaces. +sub trim { + my ($str) = @_; + if ($str) { + $str =~ s/[\r\n\t\s]+/ /g; + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; +} + +# This originally came from CGI.pm, by Lincoln D. Stein +sub url_quote { + my ($toencode) = (@_); + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; + return $toencode; +} + +################### +# Setup Functions # +################### + +sub get_config { + # read the test configuration file + my $conf_file = CONF_FILE; + my $config = do($conf_file) + or die "can't read configuration '$conf_file': $!$@"; + return $config; +} + +sub get_selenium { + my $chrome_mode = shift; + my $config = get_config(); + + require Test::WWW::Selenium; + require WWW::Selenium::Util; + + if (!WWW::Selenium::Util::server_is_running()) { + die "Selenium Server isn't running!"; + } + + my $sel = Test::WWW::Selenium->new( + host => $config->{host}, + port => $config->{port}, + browser => $chrome_mode ? $config->{experimental_browser_launcher} : $config->{browser}, + browser_url => $config->{browser_url} + ); + + return ($sel, $config); +} + +sub get_xmlrpc_client { + my $config = get_config(); + my $xmlrpc_url = $config->{browser_url} . "/" . + $config->{bugzilla_installation} . "/xmlrpc.cgi"; + + require QA::RPC::XMLRPC; + my $rpc = QA::RPC::XMLRPC->new(proxy => $xmlrpc_url); + return ($rpc, $config); +} + +sub get_jsonrpc_client { + my ($get_mode) = @_; + require QA::RPC::JSONRPC; + my $rpc = QA::RPC::JSONRPC->new(); + # If we don't set a long timeout, then the Bug.add_comment test + # where we add a too-large comment fails. + $rpc->transport->timeout(180); + $rpc->version($get_mode ? '1.1' : '1.0'); + $rpc->bz_get_mode($get_mode); + return $rpc; +} + +sub get_rpc_clients { + my ($xmlrpc, $config) = get_xmlrpc_client(); + my $jsonrpc = get_jsonrpc_client(); + my $jsonrpc_get = get_jsonrpc_client('GET'); + return ($config, $xmlrpc, $jsonrpc, $jsonrpc_get); +} + +################################ +# Helpers for Selenium Scripts # +################################ + +sub go_to_home { + my ($sel, $config) = @_; + $sel->open_ok("/$config->{bugzilla_installation}/", undef, "Go to the home page"); + $sel->title_is("Bugzilla Main Page"); +} + +# Go to the home/login page and log in. +sub log_in { + my ($sel, $config, $user) = @_; + + go_to_home($sel, $config); + $sel->type_ok("Bugzilla_login_top", $config->{"${user}_user_login"}, "Enter $user login name"); + $sel->type_ok("Bugzilla_password_top", $config->{"${user}_user_passwd"}, "Enter $user password"); + $sel->click_ok("log_in_top", undef, "Submit credentials"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Bugzilla Main Page", "User is logged in"); +} + +# Log out. Will fail if you are not logged in. +sub logout { + my $sel = shift; + + $sel->click_ok("link=Log out", undef, "Logout"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Logged Out"); +} + +# Display the bug form to enter a bug in the given product. +sub file_bug_in_product { + my ($sel, $product, $classification) = @_; + + $classification ||= "Unclassified"; + $sel->click_ok("link=New", undef, "Go create a new bug"); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($sel->is_text_present("Select Classification")) { + ok(1, "More than one enterable classification available. Display them in a list"); + $sel->click_ok("link=$classification", undef, "Choose $classification"); + $sel->wait_for_page_to_load(WAIT_TIME); + $title = $sel->get_title(); + } + if ($title eq "Enter Bug") { + ok(1, "Display the list of enterable products"); + $sel->click_ok("link=$product", undef, "Choose $product"); + $sel->wait_for_page_to_load(WAIT_TIME); + } + else { + ok(1, "Only one product available in $classification. Skipping the 'Choose product' page.") + } + $sel->title_is("Enter Bug: $product", "Display form to enter bug data"); +} + +sub create_bug { + my ($sel, $bug_summary) = @_; + my $ndash = NDASH; + + $sel->click_ok('commit'); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + my $bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); + $sel->title_like(qr/$bug_id $ndash( \(.*\))? $bug_summary/, "Bug $bug_id created with summary '$bug_summary'"); + return $bug_id; +} + +sub edit_bug { + my ($sel, $bug_id, $bug_summary, $options) = @_; + my $ndash = NDASH; + my $btn_id = $options ? $options->{id} : 'commit'; + + $sel->click_ok($btn_id); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("$bug_id $ndash $bug_summary", "Changes submitted to bug $bug_id"); + # If the web browser doesn't support history.ReplaceState or has it turned off, + # "Bug FIXME processed" is displayed instead (as in Bugzilla 4.0 and older). + # $sel->title_is("Bug $bug_id processed", "Changes submitted to bug $bug_id"); +} + +sub edit_bug_and_return { + my ($sel, $bug_id, $bug_summary, $options) = @_; + my $ndash = NDASH; + edit_bug($sel, $bug_id, $bug_summary, $options); + $sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug_id')]"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("$bug_id $ndash $bug_summary", "Returning back to bug $bug_id"); +} + +# Go to show_bug.cgi. +sub go_to_bug { + my ($sel, $bug_id) = @_; + + $sel->type_ok("quicksearch_top", $bug_id); + $sel->click_ok("find_top", undef, "Go to bug $bug_id"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + my $bug_title = $sel->get_title(); + utf8::encode($bug_title) if utf8::is_utf8($bug_title); + $sel->title_like(qr/^$bug_id /, $bug_title); +} + +# Go to admin.cgi. +sub go_to_admin { + my $sel = shift; + + $sel->click_ok("link=Administration", undef, "Go to the Admin page"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_like(qr/^Administer your installation/, "Display admin.cgi"); +} + +# Go to editproducts.cgi and display the given product. +sub edit_product { + my ($sel, $product, $classification) = @_; + + $classification ||= "Unclassified"; + go_to_admin($sel); + $sel->click_ok("link=Products", undef, "Go to the Products page"); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($title eq "Select Classification") { + ok(1, "More than one enterable classification available. Display them in a list"); + $sel->click_ok("link=$classification", undef, "Choose $classification"); + $sel->wait_for_page_to_load(WAIT_TIME); + } + else { + $sel->title_is("Select product", "Display the list of enterable products"); + } + $sel->click_ok("link=$product", undef, "Choose $product"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Edit Product '$product'", "Display properties of $product"); +} + +sub add_product { + my ($sel, $classification) = @_; + + $classification ||= "Unclassified"; + go_to_admin($sel); + $sel->click_ok("link=Products", undef, "Go to the Products page"); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($title eq "Select Classification") { + ok(1, "More than one enterable classification available. Display them in a list"); + $sel->click_ok("//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]", + undef, "Add product to $classification"); + } + else { + $sel->title_is("Select product", "Display the list of enterable products"); + $sel->click_ok("link=Add", undef, "Add a new product"); + } + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Add Product", "Display the new product form"); +} + +sub open_advanced_search_page { + my $sel = shift; + + $sel->click_ok("link=Search"); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($title eq "Simple Search") { + ok(1, "Display the simple search form"); + $sel->click_ok("link=Advanced Search"); + $sel->wait_for_page_to_load(WAIT_TIME); + } + $sel->title_is("Search for bugs", "Display the Advanced search form"); +} + +# $params is a hashref of the form: +# {section1 => { param1 => {type => '(text|select)', value => 'foo'}, +# param2 => {type => '(text|select)', value => 'bar'}, +# param3 => undef }, +# section2 => { param4 => ...}, +# } +# section1, section2, ... is the name of the section +# param1, param2, ... is the name of the parameter (which must belong to the given section) +# type => 'text' is for text fields +# type => 'select' is for drop-down select fields +# undef is for radio buttons (in which case the parameter must be the ID of the radio button) +# value => 'foo' is the value of the parameter (either text or label) +sub set_parameters { + my ($sel, $params) = @_; + + go_to_admin($sel); + $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Configuration: Required Settings"); + my $last_section = "Required Settings"; + + foreach my $section (keys %$params) { + if ($section ne $last_section) { + $sel->click_ok("link=$section"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Configuration: $section"); + $last_section = $section; + } + my $param_list = $params->{$section}; + foreach my $param (keys %$param_list) { + my $data = $param_list->{$param}; + if (defined $data) { + my $type = $data->{type}; + my $value = $data->{value}; + + if ($type eq 'text') { + $sel->type_ok($param, $value); + } + elsif ($type eq 'select') { + $sel->select_ok($param, "label=$value"); + } + else { + ok(0, "Unknown parameter type: $type"); + } + } + else { + # If the value is undefined, then the param name is + # the ID of the radio button. + $sel->click_ok($param); + } + } + $sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Parameters Updated"); + } +} + +1; + +__END__ diff --git a/xt/rest/bugzilla.t b/xt/rest/bugzilla.t new file mode 100644 index 000000000..a176d1cf1 --- /dev/null +++ b/xt/rest/bugzilla.t @@ -0,0 +1,60 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +####################################### +# Tests for REST calls in Bugzilla.pm # +####################################### + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More tests => 11; +use QA::REST; + +my $rest = get_rest_client(); +my $config = $rest->bz_config; + +my $version = $rest->call('version')->{version}; +ok($version, "GET /rest/version returns $version"); + +my $extensions = $rest->call('extensions')->{extensions}; +isa_ok($extensions, 'HASH', 'GET /rest/extensions'); +my @ext_names = sort keys %$extensions; +# There is always at least the QA extension enabled. +ok(scalar(@ext_names), scalar(@ext_names) . ' extension(s) found: ' . join(', ', @ext_names)); +ok($extensions->{QA}, 'The QA extension is enabled, with version ' . $extensions->{QA}->{version}); + +my $timezone = $rest->call('timezone')->{timezone}; +ok($timezone, "GET /rest/timezone retuns $timezone"); + +my $time = $rest->call('time'); +foreach my $type (qw(db_time web_time)) { + ok($time->{$type}, "GET /rest/time returns $type = " . $time->{$type}); +} + +# Logged-out users can only access the maintainer and requirelogin parameters. +my $params = $rest->call('parameters')->{parameters}; +my @param_names = sort keys %$params; +ok(@param_names == 2 && defined $params->{maintainer} && defined $params->{requirelogin}, + 'Only 2 parameters accessible to logged-out users: ' . join(', ', @param_names)); + +# Powerless users can access much more parameters. +$params = $rest->call('parameters', { api_key => $config->{unprivileged_user_api_key} })->{parameters}; +@param_names = sort keys %$params; +ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to powerless users'); + +# Admins can access all parameters. +$params = $rest->call('parameters', { api_key => $config->{admin_user_api_key} })->{parameters}; +@param_names = sort keys %$params; +ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to admins'); + +my $timestamp = $rest->call('last_audit_time')->{last_audit_time}; +ok($timestamp, "GET /rest/last_audit_time returns $timestamp"); diff --git a/xt/rest/classification.t b/xt/rest/classification.t new file mode 100644 index 000000000..d006de984 --- /dev/null +++ b/xt/rest/classification.t @@ -0,0 +1,61 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +############################################# +# Tests for REST calls in Classification.pm # +############################################# + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More tests => 7; +use QA::REST; + +my $rest = get_rest_client(); +my $config = $rest->bz_config; +my $args = { api_key => $config->{admin_user_api_key} }; + +my $params = $rest->call('parameters', $args)->{parameters}; +my $use_class = $params->{useclassification}; +ok(defined($use_class), 'Classifications are ' . ($use_class ? 'enabled' : 'disabled')); + +# Admins can always access classifications, even when they are disabled. +my $class = $rest->call('classification/1', $args)->{classifications}->[0]; +ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'"); +my @products = sort map { $_->{name} } @{ $class->{products} }; +ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products)); + +$class = $rest->call('classification/Class2_QA', $args)->{classifications}->[0]; +ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'"); +@products = sort map { $_->{name} } @{ $class->{products} }; +ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products)); + +if ($use_class) { + # When classifications are enabled, everybody can query classifications... + # ... including logged-out users. + $class = $rest->call('classification/1')->{classifications}->[0]; + ok($class->{id}, 'Logged-out users can access classification ' . $class->{name}); + # ... and non-admins. + $class = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} })->{classifications}->[0]; + ok($class->{id}, 'Non-admins can access classification ' . $class->{name}); +} +else { + # When classifications are disabled, only users in the 'editclassifications' + # group can access this method... + # ... logged-out users get an error. + my $error = $rest->call('classification/1', undef, undef, MUST_FAIL); + ok($error->{error} && $error->{code} == 900, + 'Logged-out users cannot query classifications when disabled: ' . $error->{message}); + # ... as well as non-admins. + $error = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} }, undef, MUST_FAIL); + ok($error->{error} && $error->{code} == 900, + 'Non-admins cannot query classifications when disabled: ' . $error->{message}); +} diff --git a/xt/search.t b/xt/search.t deleted file mode 100644 index 8f6e2e998..000000000 --- a/xt/search.t +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/perl -w -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -# For a description of this test, see Bugzilla::Test::Search -# in xt/lib/. - -use strict; -use warnings; -use lib qw(. xt/lib lib local/lib/perl5); -use Bugzilla; -use Bugzilla::Constants; -use Bugzilla::Test::Search; -use Getopt::Long; -use Pod::Usage; - -use Test::More; - -my %switches; -GetOptions(\%switches, 'operators=s', 'top-operators=s', 'long', - 'add-custom-fields', 'help|h') || die $@; - -pod2usage(verbose => 1) if $switches{'help'}; - -plan skip_all => "BZ_WRITE_TESTS environment variable not set" - if !$ENV{BZ_WRITE_TESTS}; - -Bugzilla->usage_mode(USAGE_MODE_TEST); - -my $test = new Bugzilla::Test::Search(\%switches); -plan tests => $test->num_tests; -$test->run(); - -__END__ - -=head1 NAME - -search.t - Test L - -=head1 DESCRIPTION - -This test tests L. - -Note that users may be prevented from writing new bugs, products, components, -etc. to your database while this test is running. - -=head1 OPTIONS - -=over - -=item --long - -Run AND and OR tests in addition to normal tests. Specifying ---long without also specifying L is likely to -run your system out of memory. - -=item --add-custom-fields - -This adds every type of custom field to the database, so that they can -all be tested. Note that this B, so do not use this -switch on a production installation. - -=item --operators=a,b,c - -Limit the test to testing only the listed operators. - -=item --top-operators=a,b,c - -Limit the top-level tested operators to the following list. This -means that for normal tests, only the listed operators will be tested. -However, for OR and AND tests, all other operators will be tested -along with the operators you listed. - -=item --help - -Display this help. - -=back diff --git a/xt/selenium/bug_edit.t b/xt/selenium/bug_edit.t new file mode 100644 index 000000000..46e7e6cb3 --- /dev/null +++ b/xt/selenium/bug_edit.t @@ -0,0 +1,441 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More "no_plan"; + +use QA::Util; + +my ($sel, $config) = get_selenium(); + +log_in($sel, $config, 'admin'); +set_parameters($sel, { "Bug Fields" => {"usestatuswhiteboard-on" => undef} }); + +# Clear the saved search, in case this test didn't complete previously. +if ($sel->is_text_present("My bugs from QA_Selenium")) { + $sel->click_ok("link=My bugs from QA_Selenium"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Bug List: My bugs from QA_Selenium"); + $sel->click_ok("forget_search"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Search is gone"); + $sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone"); +} + +# Just in case the test failed before completion previously, reset the CANEDIT bit. +go_to_admin($sel); +$sel->click_ok("link=Groups"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit Groups"); +$sel->click_ok("link=Master"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Change Group: Master"); +my $group_url = $sel->get_location(); +$group_url =~ /group=(\d+)$/; +my $master_gid = $1; + +clear_canedit_on_testproduct($sel, $master_gid); +logout($sel); + +# First create a bug. + +log_in($sel, $config, 'QA_Selenium_TEST'); +file_bug_in_product($sel, 'TestProduct'); +my $bug_summary = "Test bug editing"; +$sel->select_ok("bug_severity", "label=critical"); +$sel->type_ok("short_desc", $bug_summary); +$sel->type_ok("comment", "ploc"); +my $bug1_id = create_bug($sel, $bug_summary); + +# Now edit field values of the bug you just filed. + +$sel->select_ok("rep_platform", "label=Other"); +$sel->select_ok("op_sys", "label=Other"); +$sel->select_ok("priority", "label=Highest"); +$sel->select_ok("bug_severity", "label=blocker"); +$sel->type_ok("bug_file_loc", "foo.cgi?action=bar"); +$sel->type_ok("status_whiteboard", "[Selenium was here]"); +$sel->type_ok("comment", "new comment from me :)"); +$sel->select_ok("bug_status", "label=RESOLVED"); +edit_bug($sel, $bug1_id, $bug_summary); + +# Now move the bug into another product, which has a mandatory group. + +$sel->click_ok("link=$bug1_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_like(qr/^$bug1_id /); +$sel->select_ok("product", "label=QA-Selenium-TEST"); +$sel->type_ok("comment", "moving to QA-Selenium-TEST"); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Verify New Product Details..."); +$sel->select_ok("component", "label=QA-Selenium-TEST"); +$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'); +ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable"); +$sel->is_checked_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'); +edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "change_product"}); +$sel->select_ok("bug_severity", "label=normal"); +$sel->select_ok("priority", "label=High"); +$sel->select_ok("rep_platform", "label=All"); +$sel->select_ok("op_sys", "label=All"); +$sel->click_ok("cc_edit_area_showhide"); +$sel->type_ok("newcc", $config->{admin_user_login}); +$sel->type_ok("comment", "Unchecking the reporter_accessible checkbox"); +# This checkbox is checked by default. +$sel->click_ok("reporter_accessible"); +$sel->select_ok("bug_status", "label=VERIFIED"); +edit_bug_and_return($sel, $bug1_id, $bug_summary); +$sel->type_ok("comment", "I am the reporter, but I can see the bug anyway as I belong to the mandatory group"); +edit_bug($sel, $bug1_id, $bug_summary); +logout($sel); + +# The admin is not in the mandatory group, but he has been CC'ed, +# so he can view and edit the bug (as he has editbugs privs by inheritance). + +log_in($sel, $config, 'admin'); +go_to_bug($sel, $bug1_id); +$sel->select_ok("bug_severity", "label=blocker"); +$sel->select_ok("priority", "label=Highest"); +$sel->type_ok("status_whiteboard", "[Selenium was here][admin too]"); +$sel->select_ok("bug_status", "label=CONFIRMED"); +$sel->click_ok("bz_assignee_edit_action"); +$sel->type_ok("assigned_to", $config->{admin_user_login}); +$sel->type_ok("comment", "I have editbugs privs. Taking!"); +edit_bug_and_return($sel, $bug1_id, $bug_summary); +$sel->click_ok("cc_edit_area_showhide"); +$sel->type_ok("newcc", $config->{unprivileged_user_login}); +edit_bug($sel, $bug1_id, $bug_summary); +logout($sel); + +# The powerless user can see the restricted bug, as he has been CC'ed. + +log_in($sel, $config, 'unprivileged'); +go_to_bug($sel, $bug1_id); +$sel->is_text_present_ok("I have editbugs privs. Taking!"); +logout($sel); + +# Now turn off cclist_accessible, which will prevent +# the powerless user to see the bug again. + +log_in($sel, $config, 'admin'); +go_to_bug($sel, $bug1_id); +$sel->click_ok("cclist_accessible"); +$sel->type_ok("comment", "I am allowed to turn off cclist_accessible despite not being in the mandatory group"); +edit_bug($sel, $bug1_id, $bug_summary); +logout($sel); + +# The powerless user cannot see the restricted bug anymore. + +log_in($sel, $config, 'unprivileged'); +$sel->type_ok("quicksearch_top", $bug1_id); +$sel->click_ok("find_top"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug Access Denied"); +$sel->is_text_present_ok("You are not authorized to access bug #$bug1_id"); +logout($sel); + +# Move the bug back to TestProduct, which has no group restrictions. + +log_in($sel, $config, 'admin'); +go_to_bug($sel, $bug1_id); +$sel->select_ok("product", "label=TestProduct"); +# When selecting a new product, Bugzilla tries to reassign the bug by default, +# so we have to uncheck it. +$sel->click_ok("set_default_assignee"); +$sel->uncheck_ok("set_default_assignee"); +$sel->type_ok("comment", "-> Moving back to Testproduct."); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Verify New Product Details..."); +$sel->select_ok("component", "label=TestComponent"); +$sel->is_text_present_ok("These groups are not legal for the 'TestProduct' product or you are not allowed to restrict bugs to these groups"); +$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'); +ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable"); +ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not selected"); +$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]'); +$sel->is_editable_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]'); +ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="Master"]'), "Master group not selected by default"); +edit_bug($sel, $bug1_id, $bug_summary, {id => "change_product"}); +logout($sel); + +# The unprivileged user can view the bug again, but cannot +# edit it, except adding comments. + +log_in($sel, $config, 'unprivileged'); +go_to_bug($sel, $bug1_id); +$sel->type_ok("comment", "I have no privs, I can only comment (and remove people from the CC list)"); +ok(!$sel->is_element_present('//select[@name="product"]'), "Product field not editable"); +ok(!$sel->is_element_present('//select[@name="bug_severity"]'), "Severity field not editable"); +ok(!$sel->is_element_present('//select[@name="priority"]'), "Priority field not editable"); +ok(!$sel->is_element_present('//select[@name="op_sys"]'), "OS field not editable"); +ok(!$sel->is_element_present('//select[@name="rep_platform"]'), "Hardware field not editable"); +$sel->click_ok("cc_edit_area_showhide"); +$sel->add_selection_ok("cc", "label=" . $config->{admin_user_login}); +$sel->click_ok("removecc"); +edit_bug($sel, $bug1_id, $bug_summary); +logout($sel); + +# Now let's test the CANEDIT bit. + +log_in($sel, $config, 'admin'); +edit_product($sel, "TestProduct"); +$sel->click_ok("link=Edit Group Access Controls:"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit Group Controls for TestProduct"); +$sel->check_ok("canedit_$master_gid"); +$sel->click_ok("submit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Update group access controls for TestProduct"); + +# The user is in the master group, so he can comment. + +go_to_bug($sel, $bug1_id); +$sel->type_ok("comment", "Do nothing except adding a comment..."); +edit_bug($sel, $bug1_id, $bug_summary); +logout($sel); + +# This user is not in the master group, so he cannot comment. + +log_in($sel, $config, 'QA_Selenium_TEST'); +go_to_bug($sel, $bug1_id); +$sel->type_ok("comment", "Just a comment too..."); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Product Edit Access Denied"); +$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct."); +logout($sel); + +# Test searches and "format for printing". + +log_in($sel, $config, 'admin'); +open_advanced_search_page($sel); +$sel->remove_all_selections_ok("product"); +$sel->add_selection_ok("product", "TestProduct"); +$sel->remove_all_selections_ok("bug_status"); +$sel->remove_all_selections_ok("resolution"); +$sel->check_ok("emailassigned_to1"); +$sel->select_ok("emailtype1", "label=is"); +$sel->type_ok("email1", $config->{admin_user_login}); +$sel->check_ok("emailassigned_to2"); +$sel->check_ok("emailqa_contact2"); +$sel->check_ok("emailcc2"); +$sel->select_ok("emailtype2", "label=is"); +$sel->type_ok("email2", $config->{QA_Selenium_TEST_user_login}); +$sel->click_ok("Search"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List"); + +$sel->is_text_present_ok("One bug found."); +$sel->type_ok("save_newqueryname", "My bugs from QA_Selenium"); +$sel->click_ok("remember"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Search created"); +$sel->is_text_present_ok("OK, you have a new search named My bugs from QA_Selenium."); +$sel->click_ok("link=My bugs from QA_Selenium"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List: My bugs from QA_Selenium"); +$sel->click_ok("long_format"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Full Text Bug Listing"); +$sel->is_text_present_ok("Bug $bug1_id"); +$sel->is_text_present_ok("Status: CONFIRMED"); +$sel->is_text_present_ok("Reporter: QA-Selenium-TEST <$config->{QA_Selenium_TEST_user_login}>"); +$sel->is_text_present_ok("Assignee: admin <$config->{admin_user_login}>"); +$sel->is_text_present_ok("Severity: blocker"); +$sel->is_text_present_ok("Priority: Highest"); +$sel->is_text_present_ok("I have no privs, I can only comment"); +logout($sel); + +# Let's create a 2nd bug by this user so that we can test mass-change +# using the saved search the admin just created. + +log_in($sel, $config, 'QA_Selenium_TEST'); +file_bug_in_product($sel, 'TestProduct'); +my $bug_summary2 = "New bug from me"; +$sel->select_ok("bug_severity", "label=blocker"); +$sel->type_ok("short_desc", $bug_summary2); +# We turned on the CANEDIT bit for TestProduct. +$sel->type_ok("comment", "I can enter a new bug, but not edit it, right?"); +my $bug2_id = create_bug($sel, $bug_summary2); + +# Clicking the "Back" button and resubmitting the form again should trigger a warning. + +$sel->go_back_ok(); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Enter Bug: TestProduct"); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Suspicious Action"); +$sel->is_text_present_ok("no valid token for the create_bug action while processing the 'post_bug.cgi' script"); +$sel->click_ok("confirm"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_like(qr/\d+ \S $bug_summary2/, "Bug created"); +$sel->type_ok("comment", "New comment not allowed"); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Product Edit Access Denied"); +$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct."); +logout($sel); + +# Reassign the newly created bug to the admin. + +log_in($sel, $config, 'admin'); +go_to_bug($sel, $bug2_id); +$sel->click_ok("bz_assignee_edit_action"); +$sel->type_ok("assigned_to", $config->{admin_user_login}); +$sel->type_ok("comment", "Taking!"); +edit_bug($sel, $bug2_id, $bug_summary2); + +# Test mass-change. + +$sel->click_ok("link=My bugs from QA_Selenium"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List: My bugs from QA_Selenium"); +$sel->is_text_present_ok("2 bugs found"); +$sel->click_ok("mass_change"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List"); +$sel->click_ok("check_all"); +$sel->type_ok("comment", 'Mass change"'); +$sel->select_ok("bug_status", "label=RESOLVED"); +$sel->select_ok("resolution", "label=WORKSFORME"); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bugs processed"); + +$sel->click_ok("link=$bug1_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_like(qr/$bug1_id /); +$sel->selected_label_is("resolution", "WORKSFORME"); +$sel->select_ok("resolution", "label=INVALID"); +edit_bug_and_return($sel, $bug1_id, $bug_summary); +$sel->selected_label_is("resolution", "INVALID"); + +$sel->click_ok("link=History"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Changes made to bug $bug1_id"); +$sel->is_text_present_ok("URL foo.cgi?action=bar"); +$sel->is_text_present_ok("Severity critical blocker"); +$sel->is_text_present_ok("Whiteboard [Selenium was here] [Selenium was here][admin too]"); +$sel->is_text_present_ok("Product QA-Selenium-TEST TestProduct"); +$sel->is_text_present_ok("Status CONFIRMED RESOLVED"); + +# Last step: move bugs to another DB, if the extension is enabled. + +if ($config->{test_extensions}) { + set_parameters($sel, { "Bug Moving" => {"move-to-url" => {type => "text", value => 'http://www.foo.com/'}, + "move-to-address" => {type => "text", value => 'import@foo.com'}, + "movers" => {type => "text", value => $config->{admin_user_login}} + } + }); + + $sel->click_ok("link=My bugs from QA_Selenium"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Bug List: My bugs from QA_Selenium"); + $sel->is_text_present_ok("2 bugs found"); + $sel->click_ok("mass_change"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Bug List"); + $sel->click_ok("check_all"); + $sel->type_ok("comment", "-> moved"); + $sel->click_ok('oldbugmove'); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Bugs processed"); + $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); + $sel->is_text_present_ok("Changes submitted for bug $bug2_id"); + $sel->click_ok("link=$bug2_id"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_like(qr/^$bug2_id/); + $sel->selected_label_is("resolution", "MOVED"); + $sel->is_text_present_ok("Bug moved to http://www.foo.com/."); + + # Disable bug moving again. + set_parameters($sel, { "Bug Moving" => {"movers" => {type => "text", value => ""}} }); +} + +# Make sure token checks are working correctly for single bug editing and mass change, +# first with no token, then with an invalid token. + +foreach my $params (["no_token_single_bug", ""], ["invalid_token_single_bug", "&token=1"]) { + my ($comment, $token) = @$params; + $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id=$bug1_id&comment=$comment$token", + undef, "Edit a single bug with " . ($token ? "an invalid" : "no") . " token"); + $sel->title_is("Suspicious Action"); + $sel->is_text_present_ok($token ? "an invalid token" : "web browser directly"); + edit_bug_and_return($sel, $bug1_id, $bug_summary, {id => "confirm"}); + $sel->is_text_present_ok($comment); +} + +foreach my $params (["no_token_mass_change", ""], ["invalid_token_mass_change", "&token=1"]) { + my ($comment, $token) = @$params; + $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id_$bug1_id=1&id_$bug2_id=1&comment=$comment$token", + undef, "Mass change with " . ($token ? "an invalid" : "no") . " token"); + $sel->title_is("Suspicious Action"); + $sel->is_text_present_ok("no valid token for the buglist_mass_change action"); + $sel->click_ok("confirm"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Bugs processed"); + foreach my $bug_id ($bug1_id, $bug2_id) { + $sel->click_ok("link=$bug_id"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_like(qr/^$bug_id /); + $sel->is_text_present_ok($comment); + next if $bug_id == $bug2_id; + $sel->go_back_ok(); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Bugs processed"); + } +} + +# Now move these bugs out of our radar. + +$sel->click_ok("link=My bugs from QA_Selenium"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List: My bugs from QA_Selenium"); +$sel->is_text_present_ok("2 bugs found"); +$sel->click_ok("mass_change"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List"); +$sel->click_ok("check_all"); +$sel->type_ok("comment", "Reassigning to the reporter"); +$sel->type_ok("assigned_to", $config->{QA_Selenium_TEST_user_login}); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bugs processed"); + +# Now delete the saved search. + +$sel->click_ok("link=My bugs from QA_Selenium"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List: My bugs from QA_Selenium"); +$sel->click_ok("forget_search"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Search is gone"); +$sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone"); + +# Reset the CANEDIT bit. We want it to be turned off by default. +clear_canedit_on_testproduct($sel, $master_gid); +logout($sel); + +sub clear_canedit_on_testproduct { + my ($sel, $master_gid) = @_; + + edit_product($sel, "TestProduct"); + $sel->click_ok("link=Edit Group Access Controls:"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Edit Group Controls for TestProduct"); + $sel->uncheck_ok("canedit_$master_gid"); + $sel->click_ok("submit"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Update group access controls for TestProduct"); +} diff --git a/xt/selenium/choose_priority.t b/xt/selenium/choose_priority.t new file mode 100644 index 000000000..1089d2003 --- /dev/null +++ b/xt/selenium/choose_priority.t @@ -0,0 +1,30 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More "no_plan"; + +use QA::Util; + +my ($sel, $config) = get_selenium(); + +log_in($sel, $config, 'admin'); +set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-off" => undef} }); +file_bug_in_product($sel, "TestProduct"); +ok(!$sel->is_text_present("Priority"), "The Priority label is not present"); +ok(!$sel->is_element_present("//select[\@name='priority']"), "The Priority drop-down menu is not present"); +set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-on" => undef} }); +file_bug_in_product($sel, "TestProduct"); +$sel->is_text_present_ok("Priority"); +$sel->is_element_present_ok("//select[\@name='priority']"); +logout($sel); diff --git a/xt/selenium/classifications.t b/xt/selenium/classifications.t new file mode 100644 index 000000000..4d5d012f0 --- /dev/null +++ b/xt/selenium/classifications.t @@ -0,0 +1,142 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More "no_plan"; + +use QA::Util; + +my ($sel, $config) = get_selenium(); + +# Enable classifications + +log_in($sel, $config, 'admin'); +set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} }); + +# Create a new classification. + +go_to_admin($sel); +$sel->click_ok("link=Classifications"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select classification"); + +# Delete old classifications if this script failed. +# Accessing action=delete directly must 1) trigger the security check page, +# and 2) automatically reclassify products in this classification. +if ($sel->is_text_present("cone")) { + $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&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&classification=ctwo"); + $sel->title_is("Suspicious Action"); + $sel->click_ok("confirm"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Classification Deleted"); +} + +$sel->click_ok("link=Add a new classification"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add new classification"); +$sel->type_ok("classification", "cone"); +$sel->type_ok("description", "Classification number 1"); +$sel->click_ok('//input[@type="submit" and @value="Add"]'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("New Classification Created"); + +# Add TestProduct to the new classification. There should be no other +# products in this classification. + +$sel->select_ok("prodlist", "value=TestProduct"); +$sel->click_ok("add_products"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Reclassify products"); +my @products = $sel->get_select_options("myprodlist"); +ok(scalar @products == 1 && $products[0] eq 'TestProduct', "TestProduct successfully added to 'cone'"); + +# Create a new bug in this product/classification. + +file_bug_in_product($sel, 'TestProduct', 'cone'); +my $bug_summary = "Bug in classification cone"; +$sel->type_ok("short_desc", $bug_summary); +$sel->type_ok("comment", "Created by Selenium with classifications turned on"); +create_bug($sel, $bug_summary); + +# Rename 'cone' to 'Unclassified', which must be rejected as it already exists, +# then to 'ctwo', which is not yet in use. Should work fine, even with products +# already in it. + +go_to_admin($sel); +$sel->click_ok("link=Classifications"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select classification"); +$sel->click_ok("link=cone"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit classification"); +$sel->type_ok("classification", "Unclassified"); +$sel->click_ok("//input[\@value='Update']"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Classification Already Exists"); +$sel->go_back_ok(); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit classification"); +$sel->type_ok("classification", "ctwo"); +$sel->click_ok("//input[\@value='Update']"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Classification Updated"); + +# Now try to delete the 'ctwo' classification. It should fail as there are +# products in it. + +go_to_admin($sel); +$sel->click_ok("link=Classifications"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select classification"); +$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Error"); +my $error = trim($sel->get_text("error_msg")); +ok($error =~ /there are products for this classification/, "Reject classification deletion"); + +# Reclassify the product before deleting the classification. + +$sel->go_back_ok(); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select classification"); +$sel->click_ok('//a[@href="editclassifications.cgi?action=reclassify&classification=ctwo"]'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Reclassify products"); +$sel->add_selection_ok("myprodlist", "label=TestProduct"); +$sel->click_ok("remove_products"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Reclassify products"); +$sel->click_ok("link=edit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select classification"); +$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Delete classification"); +$sel->is_text_present_ok("Do you really want to delete this classification?"); +$sel->click_ok("//input[\@value='Yes, delete']"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Classification Deleted"); + +# Disable classifications and make sure you cannot edit them anymore. + +set_parameters($sel, { "Bug Fields" => {"useclassification-off" => undef} }); +$sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi"); +$sel->title_is("Classification Not Enabled"); +logout($sel); diff --git a/xt/selenium/config.t b/xt/selenium/config.t new file mode 100644 index 000000000..b99927321 --- /dev/null +++ b/xt/selenium/config.t @@ -0,0 +1,48 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More "no_plan"; + +use QA::Util; + +my ($sel, $config) = get_selenium(); + +# Turn on 'requirelogin' and log out. + +log_in($sel, $config, 'admin'); +set_parameters($sel, { "User Authentication" => {"requirelogin-on" => undef} }); +logout($sel); + +# Accessing config.cgi should display no sensitive data. + +$sel->open_ok("/$config->{bugzilla_installation}/config.cgi", undef, "Go to config.cgi (JS format)"); +$sel->is_text_present_ok("var status = [ ];"); +$sel->is_text_present_ok("var status_open = [ ];"); +$sel->is_text_present_ok("var status_closed = [ ];"); +$sel->is_text_present_ok("var resolution = [ ];"); +$sel->is_text_present_ok("var keyword = [ ];"); +$sel->is_text_present_ok("var platform = [ ];"); +$sel->is_text_present_ok("var severity = [ ];"); +$sel->is_text_present_ok("var field = [\n];"); + +ok(!$sel->is_text_present("cf_"), "No custom field displayed"); +ok(!$sel->is_text_present("component["), "No component displayed"); +ok(!$sel->is_text_present("version["), "No version displayed"); +ok(!$sel->is_text_present("target_milestone["), "No target milestone displayed"); + +# Turn on 'requirelogin' and log out. + +log_in($sel, $config, 'admin'); +set_parameters($sel, { "User Authentication" => {"requirelogin-off" => undef} }); +logout($sel); diff --git a/xt/selenium/create_user_accounts.t b/xt/selenium/create_user_accounts.t new file mode 100644 index 000000000..7c71273a6 --- /dev/null +++ b/xt/selenium/create_user_accounts.t @@ -0,0 +1,139 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More "no_plan"; + +use QA::Util; + +my ($sel, $config) = get_selenium(); + +# Set the email regexp for new bugzilla accounts to end with @bugzilla.test. + +log_in($sel, $config, 'admin'); +set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '[^@]+@bugzilla\.test$'}} }); +logout($sel); + +# Create a valid account. We need to randomize the login address, because a request +# expires after 3 days only and this test can be executed several times per day. +my $valid_account = 'selenium-' . random_string(10) . '@bugzilla.test'; + +$sel->click_ok("link=Home"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bugzilla Main Page"); +$sel->is_text_present_ok("Open a New Account"); +$sel->click_ok("link=Open a New Account"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Create a new Bugzilla account"); +$sel->type_ok("login", $valid_account); +$sel->click_ok("send"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Request for new user account '$valid_account' submitted"); +$sel->is_text_present_ok("A confirmation email has been sent"); + +# Try creating the same account again. It's too soon. +$sel->click_ok("link=Home"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bugzilla Main Page"); +$sel->is_text_present_ok("Open a New Account"); +$sel->click_ok("link=Open a New Account"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Create a new Bugzilla account"); +$sel->type_ok("login", $valid_account); +$sel->click_ok("send"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Too Soon For New Token"); +my $error_msg = trim($sel->get_text("error_msg")); +ok($error_msg =~ /Please wait 10 minutes/, "Too soon for this account"); + +# These accounts do not pass the regexp. +my @accounts = ('test@yahoo.com', 'test@bugzilla.net', 'test@bugzilla.test.com'); +foreach my $account (@accounts) { + $sel->click_ok("link=New Account"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Create a new Bugzilla account"); + $sel->type_ok("login", $account); + $sel->click_ok("send"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Account Creation Restricted"); + $sel->is_text_present_ok("User account creation has been restricted."); +} + +# These accounts are illegal. +@accounts = ('test\bugzilla@bugzilla.test', 'test@bugzilla.org@bugzilla.test', 'test@bugzilla..test'); +# Logins larger than 127 characters must be rejected, for security reasons. +push @accounts, 'selenium-' . random_string(110) . '@bugzilla.test'; + +foreach my $account (@accounts) { + $sel->click_ok("link=New Account"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Create a new Bugzilla account"); + # Starting with 5.0, the login field is a type=email and is marked "required" + # This means that we need to add the novalidate attribute to the enclosing form + # so that the illegal login can still be checked by the backend code. + my $script = q{ + document.getElementById('account_creation_form').setAttribute('novalidate', 1); + }; + $sel->run_script($script); + $sel->type_ok("login", $account); + $sel->click_ok("send"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Invalid Email Address"); + my $error_msg = trim($sel->get_text("error_msg")); + ok($error_msg =~ /^The e-mail address you entered (\S+) didn't pass our syntax checking/, "Invalid email address detected"); +} + +# This account already exists. +$sel->click_ok("link=New Account"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Create a new Bugzilla account"); +$sel->type_ok("login", $config->{admin_user_login}); +$sel->click_ok("send"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Account Already Exists"); +$error_msg = trim($sel->get_text("error_msg")); +ok($error_msg eq "There is already an account with the login name $config->{admin_user_login}.", "Account already exists"); + +# Turn off user account creation. +log_in($sel, $config, 'admin'); +set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => ''}} }); +logout($sel); + +# Make sure that links pointing to createaccount.cgi are all deactivated. +ok(!$sel->is_text_present("New Account"), "No link named 'New Account'"); +$sel->click_ok("link=Home"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bugzilla Main Page"); +ok(!$sel->is_text_present("Open a New Account"), "No link named 'Open a New Account'"); +$sel->open_ok("/$config->{bugzilla_installation}/createaccount.cgi"); +$sel->title_is("Account Creation Disabled"); +$error_msg = trim($sel->get_text("error_msg")); +ok($error_msg =~ /^User account creation has been disabled. New accounts must be created by an administrator/, + "User account creation disabled"); + +# Re-enable user account creation. + +log_in($sel, $config, 'admin'); +set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '.*'}} }); + +# Make sure selenium-@bugzilla.test has not be added to the DB yet. +go_to_admin($sel); +$sel->click_ok("link=Users"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Search users"); +$sel->type_ok("matchstr", $valid_account); +$sel->click_ok("search"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select user"); +$sel->is_text_present_ok("0 users found"); +logout($sel); diff --git a/xt/selenium/custom_fields.t b/xt/selenium/custom_fields.t new file mode 100644 index 000000000..6c0c8fa5d --- /dev/null +++ b/xt/selenium/custom_fields.t @@ -0,0 +1,462 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use 5.10.1; +use strict; +use warnings; + +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; + +use Test::More "no_plan"; + +use QA::Util; + +my ($sel, $config) = get_selenium(); +log_in($sel, $config, 'admin'); + +# Create new bug to test custom fields + +file_bug_in_product($sel, 'TestProduct'); +my $bug_summary = "What's your ID?"; +$sel->type_ok("short_desc", $bug_summary); +$sel->type_ok("comment", "Use the ID of this bug to generate a unique custom field name."); +$sel->type_ok("bug_severity", "label=normal"); +my $bug1_id = create_bug($sel, $bug_summary); + +# Create custom fields + +go_to_admin($sel); +$sel->click_ok("link=Custom Fields"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Fields"); +$sel->click_ok("link=Add a new custom field"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add a new Custom Field"); +$sel->type_ok("name", "cf_qa_freetext_$bug1_id"); +$sel->type_ok("desc", "Freetext$bug1_id"); +$sel->select_ok("type", "label=Free Text"); +$sel->type_ok("sortkey", $bug1_id); +# These values are off by default. +$sel->value_is("enter_bug", "off"); +$sel->value_is("obsolete", "off"); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Field Created"); +$sel->is_text_present_ok("The new custom field 'cf_qa_freetext_$bug1_id' has been successfully created."); + +$sel->click_ok("link=Add a new custom field"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add a new Custom Field"); +$sel->type_ok("name", "cf_qa_list_$bug1_id"); +$sel->type_ok("desc", "List$bug1_id"); +$sel->select_ok("type", "label=Drop Down"); +$sel->type_ok("sortkey", $bug1_id); +$sel->click_ok("enter_bug"); +$sel->value_is("enter_bug", "on"); +$sel->click_ok("new_bugmail"); +$sel->value_is("new_bugmail", "on"); +$sel->value_is("obsolete", "off"); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Field Created"); +$sel->is_text_present_ok("The new custom field 'cf_qa_list_$bug1_id' has been successfully created."); + +$sel->click_ok("link=Add a new custom field"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add a new Custom Field"); +$sel->type_ok("name", "cf_qa_bugid_$bug1_id"); +$sel->type_ok("desc", "Reference$bug1_id"); +$sel->select_ok("type", "label=Bug ID"); +$sel->type_ok("sortkey", $bug1_id); +$sel->type_ok("reverse_desc", "IsRef$bug1_id"); +$sel->click_ok("enter_bug"); +$sel->value_is("enter_bug", "on"); +$sel->value_is("obsolete", "off"); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Field Created"); +$sel->is_text_present_ok("The new custom field 'cf_qa_bugid_$bug1_id' has been successfully created."); + +# Add values to the custom fields. + +$sel->click_ok("link=cf_qa_list_$bug1_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)"); +$sel->click_ok("link=Edit legal values for this field"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); + +$sel->click_ok("link=Add"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->type_ok("value", "have fun?"); +$sel->type_ok("sortkey", "805"); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("New Field Value Created"); +$sel->is_text_present_ok("The value have fun? has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field."); + +$sel->click_ok("link=Add"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->type_ok("value", "storage"); +$sel->type_ok("sortkey", "49"); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("New Field Value Created"); +$sel->is_text_present_ok("The value storage has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field."); + +# Also create a new bug status and a new resolution. + +go_to_admin($sel); +$sel->click_ok("link=Field Values"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit values for which field?"); +$sel->click_ok("link=Resolution"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select value for the 'Resolution' (resolution) field"); +$sel->click_ok("link=Add"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add Value for the 'Resolution' (resolution) field"); +$sel->type_ok("value", "UPSTREAM"); +$sel->type_ok("sortkey", 450); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("New Field Value Created"); + +go_to_admin($sel); +$sel->click_ok("link=Field Values"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit values for which field?"); +$sel->click_ok("link=Status"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select value for the 'Status' (bug_status) field"); +$sel->click_ok("link=Add"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add Value for the 'Status' (bug_status) field"); +$sel->type_ok("value", "SUSPENDED"); +$sel->type_ok("sortkey", 250); +$sel->click_ok("open_status"); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("New Field Value Created"); + +$sel->click_ok("link=Add"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add Value for the 'Status' (bug_status) field"); +$sel->type_ok("value", "IN_QA"); +$sel->type_ok("sortkey", 550); +$sel->click_ok("closed_status"); +$sel->click_ok("create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("New Field Value Created"); + +$sel->click_ok("link=status workflow page"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit Workflow"); +$sel->click_ok('//td[@title="From UNCONFIRMED to SUSPENDED"]//input[@type="checkbox"]'); +$sel->click_ok('//td[@title="From CONFIRMED to SUSPENDED"]//input[@type="checkbox"]'); +$sel->click_ok('//td[@title="From SUSPENDED to CONFIRMED"]//input[@type="checkbox"]'); +$sel->click_ok('//td[@title="From SUSPENDED to IN_PROGRESS"]//input[@type="checkbox"]'); +$sel->click_ok('//td[@title="From RESOLVED to IN_QA"]//input[@type="checkbox"]'); +$sel->click_ok('//td[@title="From IN_QA to VERIFIED"]//input[@type="checkbox"]'); +$sel->click_ok('//td[@title="From IN_QA to CONFIRMED"]//input[@type="checkbox"]'); +$sel->click_ok('//input[@value="Commit Changes"]'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit Workflow"); + +# Create new bug to test custom fields in bug creation page + +file_bug_in_product($sel, 'TestProduct'); +$sel->is_text_present_ok("List$bug1_id:"); +$sel->is_element_present_ok("cf_qa_list_$bug1_id"); +$sel->is_text_present_ok("Reference$bug1_id:"); +$sel->is_element_present_ok("cf_qa_bugid_$bug1_id"); +ok(!$sel->is_text_present("Freetext$bug1_id:"), "Freetext$bug1_id is not displayed"); +ok(!$sel->is_element_present("cf_qa_freetext_$bug1_id"), "cf_qa_freetext_$bug1_id is not available"); +my $bug_summary2 = "Et de un"; +$sel->type_ok("short_desc", $bug_summary2); +$sel->select_ok("bug_severity", "critical"); +$sel->type_ok("cf_qa_bugid_$bug1_id", $bug1_id); +my $bug2_id = create_bug($sel, $bug_summary2); + +# Both fields are editable. + +$sel->type_ok("cf_qa_freetext_$bug1_id", "bonsai"); +$sel->selected_label_is("cf_qa_list_$bug1_id", "---"); +$sel->select_ok("bug_status", "label=SUSPENDED"); +edit_bug($sel, $bug2_id, $bug_summary2); + +go_to_bug($sel, $bug1_id); +$sel->type_ok("cf_qa_freetext_$bug1_id", "dumbo"); +$sel->select_ok("cf_qa_list_$bug1_id", "label=storage"); +$sel->is_text_present_ok("IsRef$bug1_id: $bug2_id"); +$sel->select_ok("bug_status", "RESOLVED"); +$sel->select_ok("resolution", "UPSTREAM"); +edit_bug_and_return($sel, $bug1_id, $bug_summary); +$sel->select_ok("bug_status", "IN_QA"); +edit_bug_and_return($sel, $bug1_id, $bug_summary); + +$sel->click_ok("link=Format For Printing"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Full Text Bug Listing"); +$sel->is_text_present_ok("Freetext$bug1_id: dumbo"); +$sel->is_text_present_ok("List$bug1_id: storage"); +$sel->is_text_present_ok("Status: IN_QA UPSTREAM"); +go_to_bug($sel, $bug2_id); +$sel->select_ok("cf_qa_list_$bug1_id", "label=storage"); +edit_bug($sel, $bug2_id, $bug_summary2); + +# Test searching for bugs using the custom fields + +open_advanced_search_page($sel); +$sel->remove_all_selections_ok("product"); +$sel->add_selection_ok("product", "TestProduct"); +$sel->remove_all_selections("bug_status"); +$sel->remove_all_selections("resolution"); +$sel->select_ok("f1", "label=List$bug1_id"); +$sel->select_ok("o1", "label=is equal to"); +$sel->type_ok("v1", "storage"); +$sel->click_ok("Search"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List"); +$sel->is_text_present_ok("2 bugs found"); +$sel->is_text_present_ok("What's your ID?"); +$sel->is_text_present_ok("Et de un"); + +# Now edit custom fields in mass changes. + +$sel->click_ok("mass_change"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bug List"); +$sel->click_ok("check_all"); +$sel->select_ok("cf_qa_list_$bug1_id", "label=---"); +$sel->type_ok("cf_qa_freetext_$bug1_id", "thanks"); +$sel->click_ok("commit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Bugs processed"); +$sel->click_ok("link=$bug2_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_like(qr/^$bug2_id/); +$sel->value_is("cf_qa_freetext_$bug1_id", "thanks"); +$sel->selected_label_is("cf_qa_list_$bug1_id", "---"); +$sel->select_ok("cf_qa_list_$bug1_id", "label=storage"); +edit_bug($sel, $bug2_id, $bug_summary2); + +# Let's now test custom field visibility. + +go_to_admin($sel); +$sel->click_ok("link=Custom Fields"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Fields"); +$sel->click_ok("link=cf_qa_list_$bug1_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)"); +$sel->select_ok("visibility_field_id", "label=Severity (bug_severity)"); +$sel->add_selection_ok("visibility_values", "label=blocker"); +$sel->add_selection_ok("visibility_values", "label=critical"); +$sel->click_ok("edit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Field Updated"); + +go_to_bug($sel, $bug1_id); +$sel->is_element_present_ok("cf_qa_list_$bug1_id", "List$bug1_id is in the DOM of the page..."); +ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... but is not displayed with severity = 'normal'"); +$sel->select_ok("bug_severity", "major"); +ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... nor with severity = 'major'"); +$sel->select_ok("bug_severity", "critical"); +$sel->is_visible_ok("cf_qa_list_$bug1_id", "... but is visible with severity = 'critical'"); +edit_bug_and_return($sel, $bug1_id, $bug_summary); +$sel->is_visible_ok("cf_qa_list_$bug1_id"); + +go_to_bug($sel, $bug2_id); +$sel->is_visible_ok("cf_qa_list_$bug1_id"); +$sel->select_ok("bug_severity", "minor"); +ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'"); +edit_bug_and_return($sel, $bug2_id, $bug_summary2); +ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'"); + +# Add a new value which is only listed under some condition. + +go_to_admin($sel); +$sel->click_ok("link=Custom Fields"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Fields"); +$sel->click_ok("link=cf_qa_list_$bug1_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)"); +$sel->select_ok("value_field_id", "label=Resolution (resolution)"); +$sel->click_ok("edit"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Custom Field Updated"); +$sel->click_ok("link=cf_qa_list_$bug1_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)"); +$sel->click_ok("link=Edit legal values for this field"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->click_ok("link=Add"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->type_ok("value", "ghost"); +$sel->type_ok("sortkey", "500"); +$sel->select_ok("visibility_value_id", "label=FIXED"); +$sel->click_ok("id=create"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("New Field Value Created"); + +go_to_bug($sel, $bug1_id); +my @labels = $sel->get_select_options("cf_qa_list_$bug1_id"); +ok(grep(/^ghost$/, @labels), "ghost is in the DOM of the page..."); +my $disabled = $sel->get_attribute("v4_cf_qa_list_$bug1_id\@disabled"); +ok($disabled, "... but is not available for selection by default"); +$sel->select_ok("bug_status", "label=RESOLVED"); +$sel->select_ok("resolution", "label=FIXED"); +$sel->select_ok("cf_qa_list_$bug1_id", "label=ghost"); +edit_bug_and_return($sel, $bug1_id, $bug_summary); +$sel->selected_label_is("cf_qa_list_$bug1_id", "ghost"); + +# Delete an unused field value. + +go_to_admin($sel); +$sel->click_ok("link=Field Values"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Edit values for which field?"); +$sel->click_ok("link=List$bug1_id"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=have%20fun%3F')]"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Delete Value 'have fun?' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->is_text_present_ok("Do you really want to delete this value?"); +$sel->click_ok("delete"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Field Value Deleted"); + +# This value cannot be deleted as it's in use. + +$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=storage')]"); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is("Delete Value 'storage' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->is_text_present_ok("There is 1 bug with this field value"); + +# Mark the