summaryrefslogtreecommitdiffstats
path: root/xt/webservice
diff options
context:
space:
mode:
Diffstat (limited to 'xt/webservice')
-rw-r--r--xt/webservice/bug_add_attachment.t231
-rw-r--r--xt/webservice/bug_add_comment.t173
-rw-r--r--xt/webservice/bug_attachments.t155
-rw-r--r--xt/webservice/bug_comments.t178
-rw-r--r--xt/webservice/bug_create.t243
-rw-r--r--xt/webservice/bug_fields.t223
-rw-r--r--xt/webservice/bug_get.t150
-rw-r--r--xt/webservice/bug_history.t33
-rw-r--r--xt/webservice/bug_legal_values.t104
-rw-r--r--xt/webservice/bug_search.t211
-rw-r--r--xt/webservice/bug_update.t705
-rw-r--r--xt/webservice/bug_update_see_also.t86
-rw-r--r--xt/webservice/bugzilla.t49
-rw-r--r--xt/webservice/group_create.t101
-rw-r--r--xt/webservice/jsonp.t34
-rw-r--r--xt/webservice/product_create.t167
-rw-r--r--xt/webservice/product_get.t113
-rw-r--r--xt/webservice/user_create.t118
-rw-r--r--xt/webservice/user_get.t222
-rw-r--r--xt/webservice/user_login_logout.t128
-rw-r--r--xt/webservice/user_offer_account_by_email.t63
21 files changed, 3487 insertions, 0 deletions
diff --git a/xt/webservice/bug_add_attachment.t b/xt/webservice/bug_add_attachment.t
new file mode 100644
index 000000000..f08e42c6c
--- /dev/null
+++ b/xt/webservice/bug_add_attachment.t
@@ -0,0 +1,231 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use MIME::Base64 qw(encode_base64 decode_base64);
+use Test::More tests => 187;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ALIAS => random_string(20);
+use constant PRIVS_USER => 'QA_Selenium_TEST';
+
+sub attach {
+ my ($id, $override) = @_;
+ my %fields = (
+ ids => [$id],
+ data => 'data-' . random_string(100),
+ file_name => 'file_name-' . random_string(60),
+ summary => 'summary-' . random_string(100),
+ content_type => 'text/plain',
+ comment => 'comment-' . random_string(100),
+ );
+
+ foreach my $key (keys %{ $override || {} }) {
+ my $value = $override->{$key};
+ if (defined $value) {
+ $fields{$key} = $value;
+ }
+ else {
+ delete $fields{$key};
+ }
+ }
+ return \%fields;
+}
+
+my ($public_bug, $private_bug) =
+ $xmlrpc->bz_create_test_bugs('private');
+my $public_id = $public_bug->{id};
+my $private_id = $private_bug->{id};
+
+my @tests = (
+ # Permissions
+ { args => attach($public_id),
+ error => 'You must log in',
+ test => 'Logged-out user cannot add an attachment to a public bug',
+ },
+ { args => attach($private_id),
+ error => "You must log in",
+ test => 'Logged-out user cannot add an attachment to a private bug',
+ },
+ { user => 'editbugs',
+ args => attach($private_id),
+ error => "not authorized to access",
+ test => "Editbugs user can't add an attachment to a private bug",
+ },
+
+ # Test ID parameter
+ { user => 'unprivileged',
+ args => attach(undef, { ids => undef }),
+ error => 'a ids argument',
+ test => 'Failing to pass the "ids" param fails',
+ },
+ { user => 'unprivileged',
+ args => attach(INVALID_BUG_ID),
+ error => "not a valid bug number",
+ test => 'Passing invalid bug id returns error "Invalid Bug ID"',
+ },
+ { user => 'unprivileged',
+ args => attach(''),
+ error => "You must enter a valid bug number",
+ test => 'Passing empty bug id returns error "Invalid Bug ID"',
+ },
+ { user => 'unprivileged',
+ args => attach(INVALID_BUG_ALIAS),
+ error => "nor an alias to a bug",
+ test => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+ },
+
+ # Test Comment parameter
+ { user => 'unprivileged',
+ args => attach($public_id, { data => undef }),
+ error => 'a data argument',
+ test => 'Failing to pass the "data" parameter fails',
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { data => '' }),
+ error => "The file you are trying to attach is empty",
+ test => 'Passing empty data fails',
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { data => random_string(300_000) }),
+ error => "Attachments cannot be more than",
+ test => "Passing an attachment that's too large fails",
+ },
+
+ # Test the private parameter
+ { user => 'unprivileged',
+ args => attach($public_id, { is_private => 1 }),
+ error => 'attachments as private',
+ test => 'Unprivileged user cannot add a private attachment'
+ },
+
+ # Content-type
+ { user => 'unprivileged',
+ args => attach($public_id, { content_type => 'foo/bar' }),
+ error => "Valid types must be of the form",
+ test => "Well-formed but invalid content type fails",
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { content_type => undef }),
+ error => 'Valid types must be of the form',
+ test => "Failing to pass content_type fails",
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { content_type => '' }),
+ error => 'Valid types must be of the form',
+ test => "Empty content type fails",
+ },
+
+ # Summary
+ { user => 'unprivileged',
+ args => attach($public_id, { summary => undef }),
+ error => 'You must enter a description for the attachment',
+ test => "Failing to pass summary fails",
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { summary => '' }),
+ error => 'You must enter a description for the attachment',
+ test => "Empty summary fails",
+ },
+
+ # Filename
+ { user => 'unprivileged',
+ args => attach($public_id, { file_name => undef }),
+ error => 'You did not specify a file to attach',
+ test => "Failing to pass file_name fails",
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { file_name => '' }),
+ error => 'You did not specify a file to attach',
+ test => "Empty file_name fails",
+ },
+
+ # Success tests
+ { user => 'unprivileged',
+ args => attach($public_id),
+ test => 'Unprivileged user can add an attachment to a public bug',
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { is_patch => 1, content_type => undef }),
+ test => 'Attaching a patch with no content type works',
+ },
+ { user => 'unprivileged',
+ args => attach($public_id, { is_patch => 1,
+ content_type => 'application/octet-stream' }),
+ test => 'Attaching a patch with a bad content_type works',
+ },
+ { user => PRIVS_USER,
+ args => attach($private_id),
+ test => 'Privileged user can add an attachment to a private bug',
+ },
+ { user => PRIVS_USER,
+ args => attach($public_id, { is_private => 1 }),
+ test => 'Insidergroup user can add a private attachment',
+ },
+);
+
+$jsonrpc_get->bz_call_fail('Bug.add_attachment', attach($public_id),
+ 'must use HTTP POST', 'add_attachment fails over GET');
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+ $rpc->bz_run_tests(tests => \@tests, method => 'Bug.add_attachment',
+ post_success => \&post_success, pre_call => \&pre_call);
+}
+
+# We have to encode data manually when using JSON-RPC, else it fails.
+sub pre_call {
+ my ($t, $rpc) = @_;
+ return if !$rpc->isa('QA::RPC::JSONRPC');
+ return if !defined $t->{args}->{data};
+
+ $t->{args}->{data} = encode_base64($t->{args}->{data}, '');
+}
+
+sub post_success {
+ my ($call, $t, $rpc) = @_;
+
+ my $ids = $call->result->{ids};
+ $call = $rpc->bz_call_success("Bug.attachments", {attachment_ids => $ids});
+ my $attachments = $call->result->{attachments};
+
+ foreach my $id (keys %$attachments) {
+ my $attachment = $attachments->{$id};
+ if ($t->{args}->{is_private}) {
+ ok($attachment->{is_private},
+ $rpc->TYPE . ": Attachment $id is private");
+ }
+ else {
+ ok(!$attachment->{is_private},
+ $rpc->TYPE . ": Attachment $id is NOT private");
+ }
+
+ if ($t->{args}->{is_patch}) {
+ is($attachment->{content_type}, 'text/plain',
+ $rpc->TYPE . ": Patch $id content type is text/plain");
+ }
+ else {
+ is($attachment->{content_type}, $t->{args}->{content_type},
+ $rpc->TYPE . ": Attachment $id content type is correct");
+ }
+
+ if ($rpc->isa('QA::RPC::JSONRPC')) {
+ # We encoded data in pre_call(), so we have to restore it to its original content.
+ $t->{args}->{data} = decode_base64($t->{args}->{data});
+ $attachment->{data} = decode_base64($attachment->{data});
+ }
+ is($attachment->{data}, $t->{args}->{data},
+ $rpc->TYPE . ": Attachment $id data is correct");
+ }
+}
diff --git a/xt/webservice/bug_add_comment.t b/xt/webservice/bug_add_comment.t
new file mode 100644
index 000000000..6f234b37a
--- /dev/null
+++ b/xt/webservice/bug_add_comment.t
@@ -0,0 +1,173 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#############################################
+# Test for xmlrpc call to Bug.add_comment() #
+#############################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use Test::More tests => 141;
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+use constant INVALID_BUG_ID => -1;
+use constant INVALID_BUG_ALIAS => 'aaaaaaa12345';
+use constant PRIVS_USER => 'QA_Selenium_TEST';
+use constant TIMETRACKING_USER => 'admin';
+
+use constant TEST_COMMENT => '--- Test Comment From QA Tests ---';
+use constant TOO_LONG_COMMENT => 'a' x 100000;
+
+my @tests = (
+ # Permissions
+ { args => { id => 'public_bug', comment => TEST_COMMENT },
+ error => 'You must log in',
+ test => 'Logged-out user cannot comment on a public bug',
+ },
+ { args => { id => 'private_bug', comment => TEST_COMMENT },
+ error => "You must log in",
+ test => 'Logged-out user cannot comment on a private bug',
+ },
+ { user => 'unprivileged',
+ args => { id => 'private_bug', comment => TEST_COMMENT },
+ error => "not authorized to access",
+ test => "Unprivileged user can't comment on a private bug",
+ },
+
+ # Test ID parameter
+ { user => 'unprivileged',
+ args => { comment => TEST_COMMENT },
+ error => 'a id argument',
+ test => 'Failing to pass the "id" param fails',
+ },
+ { user => 'unprivileged',
+ args => { id => INVALID_BUG_ID, comment => TEST_COMMENT },
+ error => "not a valid bug number",
+ test => 'Passing invalid bug id returns error "Invalid Bug ID"',
+ },
+ { user => 'unprivileged',
+ args => { id => '', comment => TEST_COMMENT },
+ error => "You must enter a valid bug number",
+ test => 'Passing empty bug id param returns error "Invalid Bug ID"',
+ },
+ { user => 'unprivileged',
+ args => { id => INVALID_BUG_ALIAS, comment => TEST_COMMENT },
+ error => "nor an alias to a bug",
+ test => 'Passing invalid bug alias returns error "Invalid Bug Alias"',
+ },
+
+ # Test Comment parameter
+ { user => 'unprivileged',
+ args => { id => 'public_bug' },
+ error => 'a comment argument',
+ test => 'Failing to pass the "comment" parameter fails',
+ },
+ { user => 'unprivileged',
+ args => { id => 'public_bug', comment => '' },
+ error => "a comment argument",
+ test => 'Passing an empty comment fails',
+ },
+ { user => 'unprivileged',
+ args => { id => 'public_bug', comment => ' ' },
+ error => 'a comment argument',
+ test => 'Passing only a space for comment fails',
+ },
+ { user => 'unprivileged',
+ args => { id => 'public_bug', comment => " \t\n\n\r\n\r\n\r " },
+ error => 'a comment argument',
+ test => 'Passing only whitespace (including newlines) fails',
+ },
+ { user => 'unprivileged',
+ args => { id => 'public_bug', comment => TOO_LONG_COMMENT },
+ error => "cannot be longer than",
+ test => "Passing a comment that's too long fails",
+ },
+
+ # Testing the "private" parameter happens in the tests for Bug.comments
+
+ # Test work_time parameter
+ # FIXME Should be testing permissions on the work_time parameter,
+ # but we currently have no way to verify whether or not time was
+ # added to the bug, and there's no error thrown if you lack perms.
+ { user => 'admin',
+ args => { id => 'public_bug', comment => TEST_COMMENT,
+ work_time => 'aaa' },
+ error => "is not a numeric value",
+ test => "Passing a non-numeric work_time fails",
+ },
+ { user => 'admin',
+ args => { id => 'public_bug', comment => TEST_COMMENT,
+ work_time => '1234567890' },
+ error => 'more than the maximum',
+ test => 'Passing too large of a work_time fails',
+ },
+ { user => 'admin',
+ args => { id => 'public_bug', comment => '',
+ work_time => '1.0' },
+ error => 'a comment argument',
+ test => 'Passing a work_time with an empty comment fails',
+ },
+
+ # Success tests
+ { user => 'unprivileged',
+ args => { id => 'public_bug', comment => TEST_COMMENT },
+ test => 'Unprivileged user can add a comment to a public bug',
+ },
+ { user => 'unprivileged',
+ args => { id => 'public_bug', comment => " \n" . TEST_COMMENT },
+ test => 'Can add a comment to a bug where the first line is whitespace',
+ },
+ { user => 'QA_Selenium_TEST',
+ args => { id => 'private_bug', comment => TEST_COMMENT },
+ test => 'Privileged user can add a comment to a private bug',
+ check_privacy => 1,
+ },
+ { user => 'QA_Selenium_TEST',
+ args => { id => 'public_bug', comment => TEST_COMMENT,
+ is_private => 1 },
+ test => 'Insidergroup user can add a private comment',
+ check_privacy => 1,
+ },
+ { user => 'admin',
+ args => { id => 'public_bug', comment => TEST_COMMENT,
+ work_time => '1.5' },
+ test => 'Timetracking user can add work_time to a bug',
+ },
+ # FIXME Need to verify that the comment added actually has work_time.
+);
+
+$jsonrpc_get->bz_call_fail('Bug.add_comment',
+ { id => 'public_bug', comment => TEST_COMMENT },
+ 'must use HTTP POST', 'add_comment fails over GET');
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+ $rpc->bz_run_tests(tests => \@tests, method => 'Bug.add_comment',
+ post_success => \&post_success);
+}
+
+sub post_success {
+ my ($call, $t, $rpc) = @_;
+ return unless $t->{check_privacy};
+
+ my $comment_id = $call->result->{id};
+ my $result = $rpc->bz_call_success('Bug.comments', {comment_ids => [$comment_id]});
+ if ($t->{args}->{is_private}) {
+ ok($result->result->{comments}->{$comment_id}->{is_private},
+ $rpc->TYPE . ": Comment $comment_id is private");
+ }
+ else {
+ ok(!$result->result->{comments}->{$comment_id}->{is_private},
+ $rpc->TYPE . ": Comment $comment_id is NOT private");
+ }
+}
diff --git a/xt/webservice/bug_attachments.t b/xt/webservice/bug_attachments.t
new file mode 100644
index 000000000..d5283685d
--- /dev/null
+++ b/xt/webservice/bug_attachments.t
@@ -0,0 +1,155 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(STANDARD_BUG_TESTS PRIVATE_BUG_USER);
+use Data::Dumper;
+use List::Util qw(first);
+use MIME::Base64;
+use Test::More tests => 313;
+my ($config, @clients) = get_rpc_clients();
+
+################
+# Bug ID Tests #
+################
+
+our %attachments;
+
+sub post_bug_success {
+ my ($call, $t) = @_;
+
+ my $bugs = $call->result->{bugs};
+ is(scalar keys %$bugs, 1, "Got exactly one bug")
+ or diag(Dumper($call->result));
+
+ my $bug_attachments = (values %$bugs)[0];
+ # Collect attachment ids
+ foreach my $alias (qw(public_bug private_bug)) {
+ foreach my $is_private (0, 1) {
+ my $find_desc = "${alias}_${is_private}";
+ my $attachment = first { $_->{summary} eq $find_desc }
+ reverse @$bug_attachments;
+ if ($attachment) {
+ $attachments{$find_desc} = $attachment->{id};
+ }
+ }
+ }
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS, method => 'Bug.attachments',
+ post_success => \&post_bug_success);
+}
+
+foreach my $alias (qw(public_bug private_bug)) {
+ foreach my $is_private (0, 1) {
+ ok($attachments{"${alias}_${is_private}"},
+ "Found attachment id for ${alias}_${is_private}");
+ }
+}
+
+####################
+# Attachment Tests #
+####################
+
+my $content_file = $config->{bugzilla_path} . '/xt/config/generate_test_data.pl';
+open(my $fh, '<', $content_file) or die "$content_file: $!";
+my $content;
+{ local $/; $content = <$fh>; }
+close($fh);
+
+# Access tests for public/private stuff, and also validate that the
+# format of each return value is correct.
+
+my @tests = (
+ # Logged-out user
+ { args => { attachment_ids => [$attachments{'public_bug_0'}] },
+ test => 'Logged-out user can access public attachment on public'
+ . ' bug by id',
+ },
+ { args => { attachment_ids => [$attachments{'public_bug_1'}] },
+ test => 'Logged-out user cannot access private attachment on public bug',
+ error => 'Sorry, you are not authorized',
+ },
+ { args => { attachment_ids => [$attachments{'private_bug_0'}] },
+ test => 'Logged-out user cannot access attachments by id on private bug',
+ error => 'You are not authorized to access',
+ },
+ { args => { attachment_ids => [$attachments{'private_bug_1'}] },
+ test => 'Logged-out user cannot access private attachment on '
+ . ' private bug',
+ error => 'You are not authorized to access',
+ },
+
+ # Logged-in, unprivileged user.
+ { user => 'unprivileged',
+ args => { attachment_ids => [$attachments{'public_bug_0'}] },
+ test => 'Logged-in user can see a public attachment on a public bug by id',
+ },
+ { user => 'unprivileged',
+ args => { attachment_ids => [$attachments{'public_bug_1'}] },
+ test => 'Logged-in user cannot access private attachment on public bug',
+ error => 'Sorry, you are not authorized',
+ },
+ { user => 'unprivileged',
+ args => { attachment_ids => [$attachments{'private_bug_0'}] },
+ test => 'Logged-in user cannot access attachments by id on private bug',
+ error => "You are not authorized to access",
+ },
+ { user => 'unprivileged',
+ args => { attachment_ids => [$attachments{'private_bug_1'}] },
+ test => 'Logged-in user cannot access private attachment on private bug',
+ error => "You are not authorized to access",
+ },
+
+ # User who can see private bugs and private attachments
+ { user => PRIVATE_BUG_USER,
+ args => { attachment_ids => [$attachments{'public_bug_1'}] },
+ test => PRIVATE_BUG_USER . ' can see private attachment on public bug',
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { attachment_ids => [$attachments{'private_bug_1'}] },
+ test => PRIVATE_BUG_USER . ' can see private attachment on private bug',
+ },
+);
+
+sub post_success {
+ my ($call, $t, $rpc) = @_;
+ is(scalar keys %{ $call->result->{attachments} }, 1,
+ "Got exactly one attachment");
+ my $attachment = (values %{ $call->result->{attachments} })[0];
+
+ cmp_ok($attachment->{last_change_time}, '=~', $rpc->DATETIME_REGEX,
+ "last_change_time is in the right format");
+ cmp_ok($attachment->{creation_time}, '=~', $rpc->DATETIME_REGEX,
+ "creation_time is in the right format");
+ is($attachment->{is_obsolete}, 0, 'is_obsolete is 0');
+ cmp_ok($attachment->{bug_id}, '=~', qr/^\d+$/, "bug_id is an integer");
+ cmp_ok($attachment->{id}, '=~', qr/^\d+$/, "id is an integer");
+ is($attachment->{content_type}, 'application/x-perl',
+ "content_type is correct");
+ cmp_ok($attachment->{file_name}, '=~', qr/^\w+\.pl$/,
+ "filename is in the expected format");
+ is($attachment->{creator}, $config->{QA_Selenium_TEST_user_login},
+ "creator is the correct user");
+ my $data = $attachment->{data};
+ $data = decode_base64($data) if $rpc->isa('QA::RPC::JSONRPC');
+ is($data, $content, 'data is correct');
+ is($attachment->{size}, length($data), "size matches data's size");
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(method => 'Bug.attachments', tests => \@tests,
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_comments.t b/xt/webservice/bug_comments.t
new file mode 100644
index 000000000..d66e445cf
--- /dev/null
+++ b/xt/webservice/bug_comments.t
@@ -0,0 +1,178 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##########################################
+# Test for xmlrpc call to Bug.comments() #
+##########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use DateTime;
+use QA::Util;
+use QA::Tests qw(STANDARD_BUG_TESTS PRIVATE_BUG_USER);
+use Test::More tests => 331;
+my ($config, @clients) = get_rpc_clients();
+
+# These gets populated when we call Bug.add_comment.
+our $creation_time;
+our %comments = (
+ public_comment_public_bug => 0,
+ public_comment_private_bug => 0,
+ private_comment_public_bug => 0,
+ private_comment_private_bug => 0,
+);
+
+sub test_comments {
+ my ($comments_returned, $call, $t, $rpc) = @_;
+
+ my $comment = $comments_returned->[0];
+ ok($comment->{bug_id}, "bug_id exists");
+ # FIXME At some point we should test attachment_id here.
+
+ if ($t->{args}->{comment_ids}) {
+ my $expected_id = $t->{args}->{comment_ids}->[0];
+ is($comment->{id}, $expected_id, "comment id is correct");
+
+ my %reverse_map = reverse %comments;
+ my $expected_text = $reverse_map{$expected_id};
+ is($comment->{text}, $expected_text, "comment has the correct text");
+
+ my $priv_login = $rpc->bz_config->{PRIVATE_BUG_USER . '_user_login'};
+ is($comment->{creator}, $priv_login, "comment creator is correct");
+
+ my $creation_day;
+ if ($rpc->isa('QA::RPC::XMLRPC')) {
+ $creation_day = $creation_time->ymd('');
+ }
+ else {
+ $creation_day = $creation_time->ymd;
+ }
+ like($comment->{time}, qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/,
+ "comment time has the right format");
+ }
+ else {
+ foreach my $field (qw(id text creator time)) {
+ ok(defined $comment->{$field}, "$field is defined");
+ }
+ }
+}
+
+################
+# Bug ID Tests #
+################
+
+sub post_bug_success {
+ my ($call, $t) = @_;
+ my @bugs = values %{ $call->result->{bugs} };
+ is(scalar @bugs, 1, "Got exactly one bug");
+ my @comments = map { @{ $_->{comments} } } @bugs;
+ test_comments(\@comments, @_);
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS, method => 'Bug.comments',
+ post_success => \&post_bug_success);
+}
+
+####################
+# Comment ID Tests #
+####################
+
+# First, create comments using add_comment.
+my @add_comment_tests;
+foreach my $key (keys %comments) {
+ $key =~ /^([a-z]+)_comment_(\w+)$/;
+ my $is_private = ($1 eq 'private' ? 1 : 0);
+ my $bug_alias = $2;
+ push(@add_comment_tests, { args => { id => $bug_alias, comment => $key,
+ private => $is_private },
+ test => "Add comment: $key",
+ user => PRIVATE_BUG_USER });
+}
+
+# Set the comment id for each comment that we add, so we can test getting
+# them back, later.
+sub post_add {
+ my ($call, $t) = @_;
+ my $key = $t->{args}->{comment};
+ $comments{$key} = $call->result->{id};
+}
+
+$creation_time = DateTime->now();
+# We only need to create these comments once, with one of the interfaces.
+$clients[0]->bz_run_tests(
+ tests => \@add_comment_tests, method => 'Bug.add_comment',
+ post_success => \&post_add);
+
+# Now check access on each private and public comment
+
+my @comment_tests = (
+ # Logged-out user
+ { args => { comment_ids => [$comments{'public_comment_public_bug'}] },
+ test => 'Logged-out user can access public comment on public bug by id',
+ },
+ { args => { comment_ids => [$comments{'private_comment_public_bug'}] },
+ test => 'Logged-out user cannot access private comment on public bug',
+ error => 'is private',
+ },
+ { args => { comment_ids => [$comments{'public_comment_private_bug'}] },
+ test => 'Logged-out user cannot access comments by id on private bug',
+ error => 'You are not authorized to access',
+ },
+ { args => { comment_ids => [$comments{'private_comment_private_bug'}] },
+ test => 'Logged-out user cannot access private comment on private bug',
+ error => 'You are not authorized to access',
+ },
+
+ # Logged-in, unprivileged user.
+ { user => 'unprivileged',
+ args => { comment_ids => [$comments{'public_comment_public_bug'}] },
+ test => 'Logged-in user can see a public comment on a public bug by id',
+ },
+ { user => 'unprivileged',
+ args => { comment_ids => [$comments{'private_comment_public_bug'}] },
+ test => 'Logged-in user cannot access private comment on public bug',
+ error => 'is private',
+ },
+ { user => 'unprivileged',
+ args => { comment_ids => [$comments{'public_comment_private_bug'}] },
+ test => 'Logged-in user cannot access comments by id on private bug',
+ error => "You are not authorized to access",
+ },
+ { user => 'unprivileged',
+ args => { comment_ids => [$comments{'private_comment_private_bug'}] },
+ test => 'Logged-in user cannot access private comment on private bug',
+ error => "You are not authorized to access",
+ },
+
+ # User who can see private bugs and private comments
+ { user => PRIVATE_BUG_USER,
+ args => { comment_ids => [$comments{'private_comment_public_bug'}] },
+ test => PRIVATE_BUG_USER . ' can see private comment on public bug',
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { comment_ids => [$comments{'private_comment_private_bug'}] },
+ test => PRIVATE_BUG_USER . ' can see private comment on private bug',
+ },
+);
+
+sub post_comments {
+ my ($call) = @_;
+ my @comments = values %{ $call->result->{comments} };
+ is(scalar @comments, 1, "Got exactly one comment");
+ test_comments(\@comments, @_);
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => \@comment_tests, method => 'Bug.comments',
+ post_success => \&post_comments);
+}
diff --git a/xt/webservice/bug_create.t b/xt/webservice/bug_create.t
new file mode 100644
index 000000000..6d7c8e14a
--- /dev/null
+++ b/xt/webservice/bug_create.t
@@ -0,0 +1,243 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+########################################
+# Test for xmlrpc call to Bug.create() #
+########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Storable qw(dclone);
+use Test::More tests => 293;
+use QA::Util;
+use QA::Tests qw(create_bug_fields PRIVATE_BUG_USER);
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+########################
+# Bug.create() testing #
+########################
+
+my $bug_fields = create_bug_fields($config);
+
+# hash to contain all the possible $bug_fields values that
+# can be passed to createBug()
+my $fields = {
+ summary => {
+ undefined => {
+ faultstring => 'You must enter a summary for this bug',
+ value => undef
+ },
+ },
+
+ product => {
+ undefined => { faultstring => 'You must select/enter a product.', value => undef },
+ invalid =>
+ { faultstring => 'does not exist', value => 'does-not-exist' },
+ },
+
+ component => {
+ undefined => {
+ faultstring => 'you must first choose a component',
+ value => undef
+ },
+ invalid => {
+ faultstring => "There is no component named 'does-not-exist'",
+ value => 'does-not-exist'
+ },
+ },
+
+ version => {
+ undefined =>
+ { faultstring => 'You must select/enter a version.', value => undef },
+ invalid => {
+ faultstring => "There is no version named 'does-not-exist' in the",
+ value => 'does-not-exist'
+ },
+ },
+ platform => {
+ undefined =>
+ { faultstring => 'You must select/enter a Hardware.',
+ value => '' },
+ invalid => {
+ faultstring => "There is no Hardware named 'does-not-exist'.",
+ value => 'does-not-exist'
+ },
+ },
+
+ status => {
+ invalid => {
+ faultstring => "There is no status named 'does-not-exist'",
+ value => 'does-not-exist'
+ },
+ },
+
+ severity => {
+ undefined =>
+ { faultstring => 'You must select/enter a Severity.',
+ value => '' },
+ invalid => {
+ faultstring => "There is no Severity named 'does-not-exist'.",
+ value => 'does-not-exist'
+ },
+ },
+
+ priority => {
+ undefined =>
+ { faultstring => 'You must select/enter a Priority.',
+ value => '' },
+ invalid => {
+ faultstring => "There is no Priority named 'does-not-exist'.",
+ value => 'does-not-exist'
+ },
+ },
+
+ op_sys => {
+ undefined => {
+ faultstring => 'You must select/enter a OS.',
+ value => ''
+ },
+ invalid => {
+ faultstring => "There is no OS named 'does-not-exist'.",
+ value => 'does-not-exist'
+ },
+ },
+
+ cc => {
+ invalid => {
+ faultstring => 'not a valid username',
+ value => ['nonuserATbugillaDOTorg']
+ },
+ },
+
+ assigned_to => {
+ invalid => {
+ faultstring => "There is no user named 'does-not-exist'",
+ value => 'does-not-exist'
+ },
+ },
+ qa_contact => {
+ invalid => {
+ faultstring => "There is no user named 'does-not-exist'",
+ value => 'does-not-exist'
+ },
+ },
+ alias => {
+ long => {
+ faultstring => 'Bug aliases cannot be longer than 40 characters',
+ value => 'MyyyyyyyyyyyyyyyyyyBugggggggggggggggggggggg'
+ },
+ existing => {
+ faultstring => 'already taken the alias',
+ value => 'public_bug'
+ },
+ numeric => {
+ faultstring => 'aliases cannot be merely numbers',
+ value => '12345'
+ },
+ commma_or_space_separated => {
+ faultstring => 'contains one or more commas or spaces',
+ value => ['Bug 12345']
+ },
+
+ },
+ groups => {
+ non_existent => {
+ faultstring => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+ value => [random_string(20)],
+ },
+ },
+ comment_is_private => {
+ invalid => {
+ faultstring => 'you are not allowed to.+comments.+private',
+ value => 1,
+ }
+ },
+};
+
+$jsonrpc_get->bz_call_fail('Bug.create', $bug_fields,
+ 'must use HTTP POST', 'create fails over GET');
+
+my @tests = (
+ { args => $bug_fields,
+ error => "You must log in",
+ test => "Cannot file bugs as a logged-out user",
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { %$bug_fields, product => 'QA-Selenium-TEST',
+ component => 'QA-Selenium-TEST',
+ target_milestone => 'QAMilestone',
+ version => 'QAVersion',
+ groups => ['QA-Selenium-TEST'],
+ # These are set here because we can't actually set them,
+ # and we need the values to be correct for post_success.
+ qa_contact => $config->{PRIVATE_BUG_USER . '_user_login'},
+ status => 'UNCONFIRMED' },
+ test => "Authorized user can file a bug against a group",
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { %$bug_fields, comment_is_private => 1,
+ # These are here because PRIVATE_BUG_USER can't set them
+ # and we need their values to be correct for post_success.
+ assigned_to => $config->{'permanent_user'},
+ qa_contact => '',
+ status => 'UNCONFIRMED' },
+ test => "Insider can create a private description"
+ },
+ { user => 'editbugs',
+ args => $bug_fields,
+ test => "Creating a bug with standard values succeeds",
+ },
+);
+
+# Convert the $fields tests into standard bz_run_tests format.
+foreach my $field (sort keys %$fields) {
+ my $test_values = $fields->{$field};
+ foreach my $test_name (sort keys %$test_values) {
+ my $input_fields = dclone($bug_fields);
+ my $check_value = $test_values->{$test_name}->{value};
+ my $error = $test_values->{$test_name}->{faultstring};
+ $input_fields->{$field} = $check_value;
+ my $test = { user => 'editbugs', args => $input_fields,
+ error => $error,
+ test => "$field $test_name: fails as expected" };
+ push(@tests, $test);
+ }
+}
+
+sub post_success {
+ my ($call, $t, $rpc) = @_;
+
+ my $id = $call->result->{id};
+ ok($id, $rpc->TYPE . ": Result has an id: $id");
+
+ my $get_call = $rpc->bz_call_success('Bug.get', { ids => [$id] });
+ my $bug = $get_call->result->{bugs}->[0];
+
+ my $expect = dclone $t->{args};
+
+ my $comment_is_private = delete $expect->{comment_is_private};
+ $expect->{creator} = $rpc->bz_config->{$t->{user} . '_user_login'};
+
+ my @fields = keys %$expect;
+ $rpc->bz_test_bug(\@fields, $bug, $expect, $t);
+
+ my $comment_call = $rpc->bz_call_success('Bug.comments', { ids => [$id] });
+ my $comment = $comment_call->result->{bugs}->{$id}->{comments}->[0];
+ is($comment->{is_private} ? 1 : 0, $comment_is_private ? 1 : 0,
+ $rpc->TYPE . ": comment privacy is correct");
+}
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+ $rpc->bz_run_tests(tests => \@tests, method => 'Bug.create',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_fields.t b/xt/webservice/bug_fields.t
new file mode 100644
index 000000000..097a607f5
--- /dev/null
+++ b/xt/webservice/bug_fields.t
@@ -0,0 +1,223 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use Test::More;
+use List::Util qw(first);
+use QA::Util;
+
+my ($config, @clients) = get_rpc_clients();
+plan tests => ($config->{test_extensions} ? 1338 : 1320);
+
+use constant INVALID_FIELD_NAME => 'invalid_field';
+use constant INVALID_FIELD_ID => -1;
+sub GLOBAL_GENERAL_FIELDS {
+ my @fields = qw(
+ attach_data.thedata
+ attachments.description
+ attachments.filename
+ attachments.isobsolete
+ attachments.ispatch
+ attachments.isprivate
+ attachments.mimetype
+ attachments.submitter
+
+ flagtypes.name
+ requestees.login_name
+ setters.login_name
+
+ alias
+ assigned_to
+ blocked
+ bug_file_loc
+ bug_group
+ bug_id
+ cc
+ cclist_accessible
+ classification
+ commenter
+ content
+ creation_ts
+ days_elapsed
+ delta_ts
+ dependson
+ everconfirmed
+ keywords
+ longdesc
+ longdescs.isprivate
+ owner_idle_time
+ product
+ qa_contact
+ reporter
+ reporter_accessible
+ see_also
+ short_desc
+ status_whiteboard
+
+ deadline
+ estimated_time
+ percentage_complete
+ remaining_time
+ work_time
+ );
+ push(@fields, 'votes') if QA::Util::get_config()->{test_extensions};
+
+ return @fields;
+}
+
+use constant STANDARD_SELECT_FIELDS =>
+ qw(bug_severity bug_status op_sys priority rep_platform resolution);
+
+use constant ALL_SELECT_FIELDS => (STANDARD_SELECT_FIELDS,
+ qw(cf_qa_status cf_single_select));
+use constant PRODUCT_FIELDS => qw(version target_milestone component);
+use constant ALL_FIELDS => (GLOBAL_GENERAL_FIELDS, ALL_SELECT_FIELDS,
+ PRODUCT_FIELDS);
+use constant MANDATORY_FIELDS => qw(short_desc product version component);
+
+use constant PUBLIC_PRODUCT => 'Another Product';
+use constant PRIVATE_PRODUCT => 'QA-Selenium-TEST';
+
+sub get_field {
+ my ($fields, $field) = @_;
+ return first { $_->{name} eq $field } @$fields;
+}
+
+sub get_products_from_field {
+ my $field = shift;
+ my %products;
+ foreach my $value (@{ $field->{values} }) {
+ foreach my $vis_value (@{ $value->{visibility_values} }) {
+ $products{$vis_value} = 1;
+ }
+ }
+ return \%products;
+}
+
+our %field_ids;
+foreach my $rpc (@clients) {
+ my $call = $rpc->bz_call_success('Bug.fields');
+ my $fields = $call->result->{fields};
+ foreach my $field (ALL_FIELDS) {
+ my $field_data = get_field($fields, $field);
+ ok($field_data, "$field is in the returned result")
+ or diag(Dumper($fields));
+ $field_ids{$field} = $field_data->{id};
+
+ if (grep($_ eq $field, MANDATORY_FIELDS)) {
+ ok($field_data->{is_mandatory}, "$field is mandatory");
+ }
+ else {
+ ok(!$field_data->{is_mandatory}, "$field is not mandatory");
+ }
+ }
+
+ foreach my $field (ALL_SELECT_FIELDS, PRODUCT_FIELDS) {
+ my $field_data = get_field($fields, $field);
+ ok(defined $field_data->{visibility_values},
+ "$field has visibility_values defined");
+ my $field_vis_undefs = grep { !defined $_ }
+ @{ $field_data->{visibility_values} };
+ is($field_vis_undefs, 0, "$field.visibility_values has no undefs")
+ or diag(Dumper($field_data->{visibility_values}));
+
+ ok(defined $field_data->{values},
+ "$field has 'values' defined");
+ my $num_values = scalar @{ $field_data->{values} };
+ ok($num_values, "$field has $num_values values");
+ # The first bug status is a fake one and has no name, so we choose the 2nd item.
+ my $first_value = $field_data->{values}->[1];
+ ok(defined $first_value->{name}, 'The first value has a name')
+ or diag(Dumper($field_data->{values}));
+ # The sortkey for milestones can be negative.
+ cmp_ok($first_value->{sortkey}, '=~', qr/^-?\d+$/,
+ "The first value has a numeric sortkey");
+
+ ok(defined $first_value->{visibility_values},
+ "$field has visibilty_values defined on its first value")
+ or diag(Dumper($field_data->{values}));
+ my @value_visibility_values = map { @{ $_->{visibility_values} } }
+ @{ $field_data->{values} };
+ my $undefs = grep { !defined $_ } @value_visibility_values;
+ is($undefs, 0,
+ "$field.values.visibility_values has no undefs");
+ }
+
+ foreach my $field (PRODUCT_FIELDS) {
+ my $field_data = get_field($fields, $field);
+ is($field_data->{value_field}, 'product',
+ "The value_field for $field is 'product'");
+ my $products = get_products_from_field($field_data);
+ ok($products->{+PUBLIC_PRODUCT},
+ "$field values are returned for the public product");
+ ok(!$products->{+PRIVATE_PRODUCT},
+ "No $field values are returned for the private product");
+ }
+}
+
+my @all_tests = (
+ { args => { ids => [values %field_ids],
+ names => [ALL_FIELDS] },
+ test => 'Getting all fields by name and id simultaneously',
+ count => scalar ALL_FIELDS
+ },
+ { args => { names => [INVALID_FIELD_NAME] },
+ error => "There is no field named",
+ test => 'Invalid field name'
+ },
+ { args => { ids => [INVALID_FIELD_ID] },
+ error => 'must be numeric',
+ test => 'Invalid field id'
+ },
+ { user => 'QA_Selenium_TEST',
+ args => { names => [PRODUCT_FIELDS] },
+ test => 'Getting product-specific fields as a privileged user',
+ count => scalar PRODUCT_FIELDS,
+ product_private_values => 1
+ },
+);
+
+foreach my $field (ALL_FIELDS) {
+ push(@all_tests,
+ { args => { names => [$field] },
+ test => "Logged-out users can get the $field field by name" });
+ push(@all_tests,
+ { args => { ids => [$field_ids{$field}] },
+ test => "Logged-out users can get the $field by id" });
+}
+
+sub post_success {
+ my ($call, $t) = @_;
+ my $fields = $call->result->{fields};
+ my $count = $t->{count};
+ $count = 1 if !defined $count;
+ is(scalar @$fields, $count, "Exactly $count field(s) returned");
+
+ if ($t->{product_private_values}) {
+ foreach my $field (@$fields) {
+ my $name = $field->{name};
+ my $field_data = get_field($fields, $name);
+ my $products = get_products_from_field($field_data);
+ ok($products->{+PUBLIC_PRODUCT},
+ "$name values are returned for the public product");
+ ok($products->{+PRIVATE_PRODUCT},
+ "$name values are returned for the private product");
+ }
+ }
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => \@all_tests, method => 'Bug.fields',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_get.t b/xt/webservice/bug_get.t
new file mode 100644
index 000000000..e05fe2cb2
--- /dev/null
+++ b/xt/webservice/bug_get.t
@@ -0,0 +1,150 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+###########################################
+# Test for xmlrpc call to Bug.get() #
+###########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use DateTime;
+use QA::Util;
+use QA::Tests qw(bug_tests PRIVATE_BUG_USER);
+use Test::More tests => 988;
+my ($config, @clients) = get_rpc_clients();
+
+my $xmlrpc = $clients[0];
+our $creation_time = DateTime->now();
+our ($public_bug, $private_bug) = $xmlrpc->bz_create_test_bugs('private');
+my $private_id = $private_bug->{id};
+my $public_id = $public_bug->{id};
+
+my $base_url = $config->{browser_url} . "/"
+ . $config->{bugzilla_installation} . '/';
+
+# Set a few fields on the private bug, including setting up
+# a dependency relationship.
+$xmlrpc->bz_log_in(PRIVATE_BUG_USER);
+$xmlrpc->bz_call_success('Bug.update', {
+ ids => [$private_id],
+ blocks => { set => [$public_id] },
+ dupe_of => $public_id,
+ is_creator_accessible => 0,
+ keywords => { set => ['test-keyword-1', 'test-keyword-2'] },
+ see_also => { add => ["${base_url}show_bug.cgi?id=$public_id",
+ "http://landfill.bugzilla.org/show_bug.cgi?id=123456"] },
+ cf_qa_status => ['in progress', 'verified'],
+ cf_single_select => 'two',
+}, 'Update the private bug');
+$xmlrpc->bz_call_success('User.logout');
+
+$private_bug->{blocks} = [$public_id];
+$private_bug->{dupe_of} = $public_id;
+$private_bug->{status} = 'RESOLVED';
+$private_bug->{is_open} = 0;
+$private_bug->{resolution} = 'DUPLICATE';
+$private_bug->{is_creator_accessible} = 0;
+$private_bug->{is_cc_accessible} = 1;
+$private_bug->{keywords} = ['test-keyword-1', 'test-keyword-2'];
+$private_bug->{see_also} = ["${base_url}show_bug.cgi?id=$public_id",
+ "http://landfill.bugzilla.org/show_bug.cgi?id=123456"];
+$private_bug->{cf_qa_status} = ['in progress', 'verified'];
+$private_bug->{cf_single_select} = 'two';
+
+$public_bug->{depends_on} = [$private_id];
+$public_bug->{dupe_of} = undef;
+$public_bug->{resolution} = '';
+$public_bug->{is_open} = 1;
+$public_bug->{is_creator_accessible} = 1;
+$public_bug->{is_cc_accessible} = 1;
+$public_bug->{keywords} = [];
+# Local Bugzilla bugs are automatically updated.
+$public_bug->{see_also} = ["${base_url}show_bug.cgi?id=$private_id"];
+$public_bug->{cf_qa_status} = [];
+$public_bug->{cf_single_select} = '---';
+
+# Fill in the timetracking fields on the public bug.
+$xmlrpc->bz_log_in('admin');
+$xmlrpc->bz_call_success('Bug.update', {
+ ids => [$public_id],
+ deadline => '2038-01-01',
+ estimated_time => '10.0',
+ remaining_time => '5.0',
+});
+$xmlrpc->bz_call_success('User.logout');
+
+# Populate other fields.
+$public_bug->{classification} = 'Unclassified';
+$private_bug->{classification} = 'Unclassified';
+$private_bug->{groups} = ['QA-Selenium-TEST'];
+$public_bug->{groups} = [];
+
+# The user filing $private_bug doesn't have permission to set the status
+# or qa_contact, so they differ from normal $public_bug values.
+$private_bug->{qa_contact} = $config->{PRIVATE_BUG_USER . '_user_login'};
+
+sub post_success {
+ my ($call, $t, $rpc) = @_;
+
+ is(scalar @{ $call->result->{bugs} }, 1, "Got exactly one bug");
+ my $bug = $call->result->{bugs}->[0];
+
+ if ($t->{user} && $t->{user} eq 'admin') {
+ ok(exists $bug->{estimated_time} && exists $bug->{remaining_time},
+ 'Admin correctly gets time-tracking fields');
+ is($bug->{deadline}, '2038-01-01', 'deadline is correct');
+ cmp_ok($bug->{estimated_time}, '==', '10.0',
+ 'estimated_time is correct');
+ cmp_ok($bug->{remaining_time}, '==', '5.0',
+ 'remaining_time is correct');
+ }
+ else {
+ ok(!exists $bug->{estimated_time} && !exists $bug->{remaining_time},
+ 'Time-tracking fields are not returned to non-privileged users');
+ }
+
+ if ($t->{user}) {
+ ok($bug->{update_token}, 'Update token returned for logged-in user');
+ }
+ else {
+ ok(!exists $bug->{update_token},
+ 'Update token not returned for logged-out users');
+ }
+
+ my $expect = $bug->{id} == $private_bug->{id} ? $private_bug : $public_bug;
+
+ my @fields = sort keys %$expect;
+ push(@fields, 'creation_time', 'last_change_time');
+
+ $rpc->bz_test_bug(\@fields, $bug, $expect, $t, $creation_time);
+}
+
+my @tests = (
+ @{ bug_tests($public_id, $private_id) },
+ { args => { ids => [$public_id],
+ include_fields => ['id', 'summary', 'groups'] },
+ test => 'include_fields',
+ },
+ { args => { ids => [$public_id],
+ exclude_fields => ['assigned_to', 'cf_qa_status'] },
+ test => 'exclude_fields' },
+ { args => { ids => [$public_id],
+ include_fields => ['id', 'summary', 'groups'],
+ exclude_fields => ['summary'] },
+ test => 'exclude_fields overrides include_fields' },
+);
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => \@tests, method => 'Bug.get',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_history.t b/xt/webservice/bug_history.t
new file mode 100644
index 000000000..02ec1c11a
--- /dev/null
+++ b/xt/webservice/bug_history.t
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#########################################
+# Test for xmlrpc call to Bug.history() #
+#########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(STANDARD_BUG_TESTS);
+use Test::More tests => 114;
+my ($config, @clients) = get_rpc_clients();
+
+sub post_success {
+ my ($call, $t) = @_;
+ is(scalar @{ $call->result->{bugs} }, 1, "Got exactly one bug");
+ isa_ok($call->result->{bugs}->[0]->{history}, 'ARRAY', "Bug's history");
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => STANDARD_BUG_TESTS,
+ method => 'Bug.history', post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_legal_values.t b/xt/webservice/bug_legal_values.t
new file mode 100644
index 000000000..2f775e528
--- /dev/null
+++ b/xt/webservice/bug_legal_values.t
@@ -0,0 +1,104 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##############################################
+# Test for xmlrpc call to Bug.legal_values() #
+##############################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 269;
+use QA::Util;
+my ($config, @clients) = get_rpc_clients();
+
+use constant INVALID_PRODUCT_ID => -1;
+use constant INVALID_FIELD_NAME => 'invalid_field';
+use constant GLOBAL_FIELDS =>
+ qw(bug_severity bug_status op_sys priority rep_platform resolution
+ cf_qa_status cf_single_select);
+use constant PRODUCT_FIELDS => qw(version target_milestone component);
+
+
+my $products = $clients[0]->bz_get_products();
+my $public_product = $products->{'Another Product'};
+my $private_product = $products->{'QA-Selenium-TEST'};
+
+my @all_tests;
+
+for my $field (GLOBAL_FIELDS) {
+ push(@all_tests,
+ { args => { field => $field },
+ test => "Logged-out user can get $field values" });
+}
+
+for my $field (PRODUCT_FIELDS) {
+ my @tests = (
+ { args => { field => $field },
+ error => "argument was not set",
+ test => "$field can't be accessed without a value for 'product'",
+ },
+ { args => { product_id => INVALID_PRODUCT_ID, field => $field },
+ error => "does not exist",
+ test => "$field cannot be accessed with an invalid product id",
+ },
+
+ { args => { product_id => $private_product, field => $field },
+ error => "you don't have access",
+ test => "Logged-out user cannot access $field in private product"
+ },
+ { args => { product_id => $public_product, field => $field },
+ test => "Logged-out user can access $field in a public product",
+ },
+
+ { user => 'unprivileged',
+ args => { product_id => $private_product, field => $field },
+ error => "you don't have access",
+ test => "Unprivileged user cannot access $field in private product",
+ },
+ { user => 'unprivileged',
+ args => { product_id => $public_product, field => $field },
+ test => "Logged-in user can access $field in public product",
+ },
+
+ { user => 'QA_Selenium_TEST',
+ args => { product_id => $private_product, field => $field },
+ test => "Privileged user can access $field in a private product",
+ },
+ );
+
+ push(@all_tests, @tests);
+}
+
+my @extra_tests = (
+ { args => { product_id => $private_product, },
+ error => "requires a field argument",
+ test => "Passing product_id without 'field' throws an error",
+ },
+ { args => { field => INVALID_FIELD_NAME },
+ error => "Can't use \"" . INVALID_FIELD_NAME . "\" as a field name",
+ test => 'Invalid field name'
+ },
+);
+
+push(@all_tests, @extra_tests);
+
+sub post_success {
+ my ($call) = @_;
+
+ cmp_ok(scalar @{ $call->result->{'values'} }, '>', 0,
+ 'Got one or more values');
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => \@all_tests, method => 'Bug.legal_values',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_search.t b/xt/webservice/bug_search.t
new file mode 100644
index 000000000..93a517e24
--- /dev/null
+++ b/xt/webservice/bug_search.t
@@ -0,0 +1,211 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+########################################
+# Test for xmlrpc call to Bug.search() #
+########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER);
+use DateTime;
+use List::MoreUtils qw(uniq);
+use Test::More;
+use Data::Dumper;
+
+my ($config, @clients) = get_rpc_clients();
+plan tests => $config->{test_extensions} ? 531 : 522;
+
+my ($public_bug, $private_bug) = $clients[0]->bz_create_test_bugs('private');
+
+# Add aliases to both bugs
+$public_bug->{alias} = random_string(40);
+$private_bug->{alias} = random_string(40);
+my $alias_tests = [
+ { user => 'editbugs',
+ args => { ids => [ $public_bug->{id} ], alias => $public_bug->{alias} },
+ test => 'Add alias to public bug' },
+ { user => PRIVATE_BUG_USER,
+ args => { ids => [ $private_bug->{id} ],
+ cc => { add => [ $config->{'editbugs_user_login'} ] } },
+ test => 'Add editusers to cc of private bug' },
+ { user => 'editbugs',
+ args => { ids => [ $private_bug->{id} ], alias => $private_bug->{alias} },
+ test => 'Add alias to private bug' },
+ { user => PRIVATE_BUG_USER,
+ args => { ids => [ $private_bug->{id} ],
+ cc => { remove => [ $config->{'editbugs_user_login'} ] } },
+ test => 'Remove editusers from cc of private bug' },
+];
+$clients[0]->bz_run_tests(tests => $alias_tests, method => 'Bug.update');
+
+my @tests;
+foreach my $field (keys %$public_bug) {
+ next if ($field eq 'cc' or $field eq 'description');
+ my $test = { args => { $field => $public_bug->{$field} },
+ test => "Search by $field" };
+ if ( grep($_ eq $field, qw(alias whiteboard summary)) ) {
+ $test->{exactly} = 1; $test->{bugs} = 1;
+ }
+ push(@tests, $test);
+}
+
+push(@tests, (
+ { args => { offset => 1 },
+ test => "Offset without limit fails",
+ error => 'requires a limit argument',
+ },
+
+ { args => { alias => $private_bug->{alias} },
+ test => 'Logged-out cannot find a private_bug by alias',
+ bugs => 0,
+ },
+
+ { args => { creation_time => '19700101T00:00:00' },
+ test => 'Get all bugs by creation time',
+ },
+ { args => { creation_time => '20380101T00:00:00' },
+ test => 'Get no bugs, by creation time',
+ bugs => 0,
+ },
+ { args => { last_change_time => '19700101T00:00:00' },
+ test => 'Get all bugs by last_change_time',
+ },
+ { args => { last_change_time => '20380101T00:00:00' },
+ test => 'Get no bugs by last_change_time',
+ bugs => 0,
+ },
+
+ { args => { reporter => $config->{editbugs_user_login} },
+ test => 'Search by reporter',
+ },
+ { args => { resolution => '' },
+ test => 'Search for empty resolution',
+ },
+ { args => { resolution => 'NO_SUCH_RESOLUTION' },
+ test => 'Search for invalid resolution',
+ bugs => 0,
+ },
+ { args => { summary => substr($public_bug->{summary}, 0, 50) },
+ test => 'Search by partial summary',
+ bugs => 1, exactly => 1
+ },
+ { args => { summary => random_string() . ' ' . random_string() },
+ test => 'Summary search that returns no results',
+ bugs => 0,
+ },
+ { args => { summary => [split(/\s/, $public_bug->{summary})] },
+ test => 'Summary search using multiple terms',
+ },
+
+ { args => { whiteboard => substr($public_bug->{whiteboard}, 0, 50) },
+ test => 'Search by partial whiteboard',
+ bugs => 1, exactly => 1,
+ },
+ { args => { whiteboard => random_string(100) },
+ test => 'Whiteboard search that returns no results',
+ bugs => 0,
+ },
+ { args => { whiteboard => [split(/\s/, $public_bug->{whiteboard})] },
+ test => 'Whiteboard search using multiple terms',
+ bugs => 1, exactly => 1,
+ },
+
+ { args => { product => $public_bug->{product},
+ component => $public_bug->{component},
+ last_change_time => '19700101T00:00:00' },
+ test => 'Search by multiple arguments',
+ },
+
+ # Logged-in user who can see private bugs
+ { user => PRIVATE_BUG_USER,
+ args => { alias => [$public_bug->{alias}, $private_bug->{alias}] },
+ test => 'Search using two aliases (including one private)',
+ bugs => 2, exactly => 1,
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { product => [$public_bug->{product}, $private_bug->{product}],
+ limit => 1 },
+ test => 'Limit 1',
+ bugs => 1, exactly => 1,
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { product => [$public_bug->{product}, $private_bug->{product}],
+ limit => 1, offset => 1 },
+ test => 'Limit 1 Offset 1',
+ bugs => 1, exactly => 1,
+ },
+
+ # include_fields ane exclude_fields
+ { args => { id => $public_bug->{id},
+ include_fields => ['id', 'alias', 'summary', 'groups'] },
+ test => 'include_fields',
+ },
+ { args => { id => $public_bug->{id},
+ exclude_fields => ['assigned_to', 'cf_qa_status'] },
+ test => 'exclude_fields' },
+ { args => { id => $public_bug->{id},
+ include_fields => ['id', 'alias', 'summary', 'groups'],
+ exclude_fields => ['summary'] },
+ test => 'exclude_fields overrides include_fields' },
+));
+
+push(@tests,
+ { args => { votes => 1 },
+ test => 'Search by votes',
+ bugs => -1, # We don't care how many it returns, for now.
+ }) if $config->{test_extensions};
+
+sub post_success {
+ my ($call, $t) = @_;
+ my $bugs = $call->result->{bugs};
+
+ my $expected_count = $t->{bugs};
+ $expected_count = 1 if !defined $expected_count;
+ if ($expected_count) {
+ my $operator = $t->{exactly} ? '==' : '>=';
+ cmp_ok(scalar @$bugs, $operator, $expected_count,
+ 'The right number of bugs are returned');
+ unless ($t->{user} and $t->{user} eq PRIVATE_BUG_USER) {
+ ok(!grep($_->{alias} && $_->{alias} eq $private_bug->{alias}, @$bugs),
+ 'Result does not contain the private bug');
+ }
+
+ my @include = @{ $t->{args}->{include_fields} || [] };
+ my @exclude = @{ $t->{args}->{exclude_fields} || [] };
+ if (@include or @exclude) {
+ my @check_fields = uniq (keys %$public_bug, @include);
+ foreach my $field (sort @check_fields) {
+ next if $field eq 'description';
+ if ((@include and !grep { $_ eq $field } @include )
+ or (@exclude and grep { $_ eq $field } @exclude))
+ {
+ ok(!exists $bugs->[0]->{$field}, "$field is not included")
+ or diag Dumper($bugs);
+ }
+ else {
+ ok(exists $bugs->[0]->{$field}, "$field is included");
+ }
+ }
+ }
+
+ }
+ else {
+ is(scalar @$bugs, 0, 'No bugs returned');
+ }
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => \@tests,
+ method => 'Bug.search', post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_update.t b/xt/webservice/bug_update.t
new file mode 100644
index 000000000..dfc2f89e1
--- /dev/null
+++ b/xt/webservice/bug_update.t
@@ -0,0 +1,705 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER STANDARD_BUG_TESTS);
+use Storable qw(dclone);
+use Test::More tests => 937;
+
+use constant NONEXISTENT_BUG => 12_000_000;
+
+###############
+# Subroutines #
+###############
+
+# We have to generate different values for each RPC client, so we
+# have a function to generate the tests for each client.
+sub get_tests {
+ my ($config, $rpc) = @_;
+
+ # update doesn't support logged-out users.
+ my @tests = grep { $_->{user} } @{ STANDARD_BUG_TESTS() };
+
+ my ($public_bug, $second_bug) = $rpc->bz_create_test_bugs();
+ my ($public_id, $second_id) = ($public_bug->{id}, $second_bug->{id});
+
+ # Add aliases to both bugs
+ $public_bug->{alias} = random_string(40);
+ $second_bug->{alias} = random_string(40);
+ my $alias_tests = [
+ { user => 'editbugs',
+ args => { ids => [ $public_id ], alias => $public_bug->{alias} },
+ test => 'Add alias to public bug' },
+ { user => 'editbugs',
+ args => { ids => [ $second_id ], alias => $second_bug->{alias} },
+ test => 'Add alias to second bug' },
+ ];
+ $rpc->bz_run_tests(tests => $alias_tests, method => 'Bug.update');
+
+ my $comment_call = $rpc->bz_call_success(
+ 'Bug.comments', { ids => [$public_id, $second_id] });
+ $public_bug->{comment} =
+ $comment_call->result->{bugs}->{$public_id}->{comments}->[0];
+ $second_bug->{comment} =
+ $comment_call->result->{bugs}->{$second_id}->{comments}->[0];
+
+ push(@tests, (
+ { args => { ids => [$public_id] },
+ error => 'You must log in',
+ test => 'Logged-out users cannot call update' },
+
+ # FIXME We need a permissions test for canedit, but it's so uncommonly
+ # used that it's not a high priority.
+ ));
+
+ my %valid = valid_values($config, $public_bug, $second_bug);
+ my $valid_value_tests = valid_values_to_tests(\%valid, $public_bug);
+ push(@tests, @$valid_value_tests);
+
+ my %invalid = invalid_values($public_bug, $second_bug);
+ my $invalid_value_tests = invalid_values_to_tests(\%invalid, $public_bug);
+ push(@tests, @$invalid_value_tests);
+
+ return \@tests;
+}
+
+sub valid_values {
+ my ($config, $public_bug, $second_bug) = @_;
+
+ my $admin = $config->{'admin_user_login'};
+ my $second_id = $second_bug->{id};
+ my $comment_id = $public_bug->{comment}->{id};
+ my $bug_uri = $config->{browser_url} . '/'
+ . $config->{bugzilla_installation} . '/show_bug.cgi?id=';
+
+ my %values = (
+ alias => [
+ { value => random_string(20) },
+ ],
+ assigned_to => [
+ { value => $config->{'unprivileged_user_login'} }
+ ],
+ blocks => [
+ { value => { set => [$second_id] },
+ added => $second_id,
+ test => 'set to second bug' },
+ { value => { remove => [$second_id] },
+ added => '', removed => $second_id,
+ test => 'remove second bug' },
+ { value => { add => [$second_id] },
+ added => $second_id, removed => '',
+ test => 'add second bug' },
+ { value => { set => [] },
+ added => '', removed => $second_id,
+ test => 'set to nothing' },
+ ],
+
+ cc => [
+ { value => { add => [$admin] },
+ added => $admin, removed => '',
+ test => 'add admin' },
+ { value => { remove => [$admin] },
+ added => '', removed => $admin,
+ test => 'remove admin' },
+ { value => { remove => [$admin] },
+ test => "removing user who isn't on the list works",
+ no_changes => 1 },
+ ],
+
+ is_cc_accessible => [
+ { value => 0, test => 'set to 0' },
+ { value => 1, test => 'set to 1' },
+ ],
+
+ comment => [
+ { value => { body => random_string(100) }, test => 'public' },
+ { value => { body => random_string(100), is_private => 1 },
+ user => PRIVATE_BUG_USER, test => 'private' },
+ ],
+
+ comment_is_private => [
+ { value => { $comment_id => 1 },
+ user => PRIVATE_BUG_USER, test => 'make description private' },
+ { value => { $comment_id => 0 },
+ user => PRIVATE_BUG_USER, test => 'make description public' },
+ ],
+
+ component => [
+ { value => 'c2' }
+ ],
+
+ deadline => [
+ { value => '2037-01-01' },
+ { value => '', removed => '2037-01-01', test => 'remove' },
+ ],
+
+ dupe_of => [
+ { value => $second_id },
+ ],
+
+ estimated_time => [
+ { value => '10.0' },
+ { value => '0.0', removed => '10.0', test => 'set to zero' },
+ ],
+
+ groups => [
+ { value => { add => ['Master'] },
+ user => 'admin', added => 'Master', test => 'add Master' },
+ { value => { remove => ['Master'] },
+ user => 'admin', added => '', removed => 'Master',
+ test => 'remove Master' },
+ ],
+
+ keywords => [
+ { value => { add => ['test-keyword-1'] },
+ test => 'add one', added => 'test-keyword-1' },
+ { value => { set => ['test-keyword-1', 'test-keyword-2'] },
+ test => 'set two', added => 'test-keyword-2' },
+ { value => { remove => ['test-keyword-1'] },
+ removed => 'test-keyword-1', added => '',
+ test => 'remove one' },
+ { value => { set => [] },
+ removed => 'test-keyword-2', added => '',
+ test => 'set to empty' },
+ { value => { remove => ['test-keyword-2'] },
+ test => 'removing removed keyword does nothing',
+ no_changes => 1 },
+ ],
+
+ op_sys => [
+ { value => 'All' },
+ ],
+
+ platform => [
+ { value => 'All' },
+ ],
+
+ priority => [
+ { value => 'Normal' },
+ ],
+
+ product => [
+ { value => 'C2 Forever',
+ extra => {
+ component => 'Helium', version => 'unspecified',
+ target_milestone => '---',
+ },
+ test => 'move to C2 Forever'
+ },
+ # This also tests that the extra fields transfer over properly
+ # when they have identical names in both products.
+ { value => $public_bug->{product},
+ extra => { component => $public_bug->{component} },
+ test => 'move back to original product' },
+ ],
+
+ qa_contact => [
+ { value => $admin },
+ { value => '', test => 'set blank', removed => $admin },
+ # Reset to the original so that reset_qa_contact can also be tested.
+ { value => $public_bug->{qa_contact} },
+ ],
+
+ remaining_time => [
+ { value => '1000.50' },
+ { value => 0 },
+ ],
+
+ reset_assigned_to => [
+ { value => 1, field => 'assigned_to',
+ added => $config->{permanent_user} },
+ ],
+
+ reset_qa_contact => [
+ { value => 1, field => 'qa_contact', added => '' },
+ ],
+
+ resolution => [
+ { value => 'FIXED', extra => { status => 'RESOLVED' },
+ test => 'to RESOLVED FIXED' },
+ { value => 'INVALID', test => 'just resolution' },
+ ],
+
+ see_also => [
+ { value => { add => [$bug_uri . $second_id] },
+ added => $bug_uri . $second_id, removed => '',
+ test => 'add local bug URI' },
+ { value => { remove => [$bug_uri . $second_id] },
+ removed => $bug_uri . $second_id, added => '',
+ test => 'remove local bug URI' },
+ { value => { remove => ['http://landfill.bugzilla.org/bugzilla-tip/show_bug.cgi?id=1'] },
+ no_changes => 1,
+ test => 'removing non-existent URI works' },
+ { value => { add => [''] },
+ no_changes => 1,
+ test => 'adding an empty string to see_also does nothing' },
+ { value => { add => [undef] },
+ no_changes => 1,
+ test => 'adding a null to see_also does nothing' },
+ ],
+
+ status => [
+ # At this point, due to previous tests, the status is RESOLVED,
+ # so changing to CONFIRMED is our only real option if we want to
+ # test a simple open status.
+ { value => 'CONFIRMED' },
+ ],
+
+ severity => [
+ { value => 'critical' },
+ ],
+
+ summary => [
+ { value => random_string(100) },
+ ],
+
+ target_milestone => [
+ { value => 'AnotherMS2' },
+ ],
+
+ url => [
+ { value => 'http://' . random_string(20) . '/' },
+ ],
+
+ version => [
+ { value => 'Another2' },
+ ],
+
+ whiteboard => [
+ { value => random_string(1000) },
+ ],
+
+ work_time => [
+ # FIXME: work_time really needs to start showing up in the changes
+ # hash.
+ { value => '1.2', no_changes => 1 },
+ { value => '-1.2', test => 'negative value', no_changes => 1 },
+ ],
+ );
+
+ $values{depends_on} = $values{blocks};
+ $values{is_creator_accessible} = $values{is_cc_accessible};
+
+ return %values;
+};
+
+sub valid_values_to_tests {
+ my ($valid_values, $public_bug) = @_;
+
+ my @tests;
+ foreach my $field (sort keys %$valid_values) {
+ my @tests_valid = @{ $valid_values->{$field} };
+ foreach my $item (@tests_valid) {
+ my $desc = $item->{test} || 'valid value';
+ my %args = (
+ ids => [$public_bug->{id}],
+ $field => $item->{value},
+ %{ $item->{extra} || {} },
+ );
+ my %test = ( user => 'editbugs', args => \%args, field => $field,
+ test => "$field: $desc" );
+ foreach my $item_field (qw(no_changes added removed field user)) {
+ next if !exists $item->{$item_field};
+ $test{$item_field} = $item->{$item_field};
+ }
+ push(@tests, \%test);
+ }
+ }
+
+ return \@tests;
+}
+
+sub invalid_values {
+ my ($public_bug, $second_bug) = @_;
+
+ my $public_id = $public_bug->{id};
+ my $second_id = $second_bug->{id};
+
+ my $comment_id = $public_bug->{comment}->{id};
+ my $second_comment_id = $second_bug->{comment}->{id};
+
+ my %values = (
+ alias => [
+ { value => random_string(41),
+ error => 'aliases cannot be longer than',
+ test => 'alias cannot be too long' },
+ { value => $second_bug->{alias},
+ error => 'has already taken the alias',
+ test => 'duplicate alias fails' },
+ { value => 123456,
+ error => 'at least one letter',
+ test => 'numeric alias fails' },
+ { value => random_string(20), ids => [$public_id, $second_id],
+ error => 'aliases when modifying multiple',
+ test => 'setting alias on multiple bugs fails' },
+ ],
+
+ assigned_to => [
+ { value => random_string(20),
+ error => 'There is no user named',
+ test => 'changing assigned_to to invalid user fails' },
+ { value => '',
+ error => 'you must provide an address for the new assignee',
+ test => 'empty assigned_to fails' },
+ # FIXME Also check strict_isolation at some point in the future,
+ # perhaps.
+ ],
+
+ blocks => [
+ { value => { add => [NONEXISTENT_BUG] },
+ error => 'does not exist',
+ test => 'Non-existent bug number fails in deps' },
+ { value => { add => [$public_id] },
+ error => 'block itself or depend on itself',
+ test => "can't add this bug itself in a dep field" },
+ # FIXME Could use strict_isolation checks at some point.
+ # FIXME Could use a dependency_loop_multi test.
+ ],
+
+ cc => [
+ { value => { add => [random_string(20)] },
+ error => 'There is no user named',
+ test => 'adding invalid user to cc fails' },
+ { value => { remove => [random_string(20)] },
+ error => 'There is no user named',
+ test => 'removing invalid user from cc fails' },
+ ],
+
+ comment => [
+ { value => { body => random_string(100_000) },
+ error => 'cannot be longer',
+ test => 'comment too long' },
+ { value => { body => random_string(100), is_private => 1 },
+ error => 'comments or attachments as private',
+ test => 'normal user cannot add private comments' },
+ ],
+
+ comment_is_private => [
+ { value => { $comment_id => 1 },
+ error => 'comments or attachments as private',
+ test => 'normal user cannot make a comment private' },
+ { value => { $second_comment_id => 1 },
+ error => 'You tried to modify the privacy of comment',
+ user => PRIVATE_BUG_USER,
+ test => 'cannot change privacy on a comment on another bug' },
+ ],
+
+ component => [
+ { value => '',
+ error => 'you must first choose a component',
+ test => 'empty component fails' },
+ { value => random_string(20),
+ error => 'There is no component named',
+ test => 'invalid component fails' },
+ ],
+
+ deadline => [
+ { value => random_string(20),
+ error => 'is not a legal date',
+ test => 'Non-date fails in deadline' },
+ { value => '2037',
+ error => 'is not a legal date',
+ test => 'year alone fails in deadline' },
+ ],
+
+ dupe_of => [
+ { value => undef,
+ error => 'dup_id was not defined',
+ test => 'undefined dupe_of fails' },
+ { value => NONEXISTENT_BUG,
+ error => 'does not exist',
+ test => 'Cannot dup to a nonexistent bug' },
+ { value => $public_id,
+ error => 'as a duplicate of itself',
+ test => 'Cannot dup bug to itself' },
+ ],
+
+ estimated_time => [
+ { value => -1,
+ error => 'less than the minimum allowable value',
+ test => 'negative estimated_time fails' },
+ { value => 100_000_000,
+ error => 'more than the maximum allowable value',
+ test => 'too-large estimated_time fails' },
+ { value => random_string(20),
+ error => 'is not a numeric value',
+ test => 'non-numeric estimated_time fails' },
+ # We use PRIVATE_BUG_USER because he can modify the bug, but
+ # can't change time-tracking fields.
+ { value => '100', user => PRIVATE_BUG_USER,
+ error => 'only a user with the required permissions',
+ test => 'non-timetracker can not set estimated_time' },
+ ],
+
+ groups => [
+ { value => { add => ['Master'] },
+ error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+ test => "adding group we don't have access to but is valid fails" },
+ { value => { add => ['QA-Selenium-TEST'] },
+ error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+ test => 'adding valid group that is not in this product fails' },
+ { value => { add => [random_string(20)] },
+ error => 'either this group does not exist, or you are not allowed to restrict bugs to this group',
+ test => 'adding non-existent group fails' },
+ { value => { remove => [random_string(20)] },
+ error => 'either this group does not exist, or you are not allowed to remove bugs from this group',
+ test => 'removing non-existent group fails' },
+ ],
+
+ keywords => [
+ { value => { add => [random_string(20)] },
+ error => 'See the list of available keywords',
+ test => 'adding invalid keyword fails' },
+ { value => { remove => [random_string(20)] },
+ error => 'See the list of available keywords',
+ test => 'removing invalid keyword fails' },
+ { value => { set => [random_string(20)] },
+ error => 'See the list of available keywords',
+ test => 'setting invalid keyword fails' },
+ ],
+
+ op_sys => [
+ { value => random_string(20),
+ error => 'There is no',
+ test => 'invalid op_sys fails' },
+ { value => '',
+ error => 'You must select/enter',
+ test => 'blank op_sys fails' },
+ ],
+
+ product => [
+ { value => random_string(60),
+ error => "does not exist or you aren't authorized",
+ test => 'invalid product fails' },
+ { value => '',
+ error => 'You must select/enter a product',
+ test => 'moving to blank product fails' },
+ { value => 'TestProduct',
+ error => 'There is no component named',
+ test => 'moving products without other fields fails' },
+ { value => 'QA-Selenium-TEST',
+ extra => { component => 'QA-Selenium-TEST' },
+ error => "does not exist or you aren't authorized",
+ test => 'moving to inaccessible product fails' },
+ { value => 'QA Entry Only',
+ error => "does not exist or you aren't authorized",
+ test => 'moving to product where ENTRY is denied fails' },
+ ],
+
+ qa_contact => [
+ { value => random_string(20),
+ error => 'There is no user named',
+ test => 'changing qa_contact to invalid user fails' },
+ ],
+
+ remaining_time => [
+ { value => -1,
+ error => 'less than the minimum allowable value',
+ test => 'negative remaining_time fails' },
+ { value => 100_000_000,
+ error => 'more than the maximum allowable value',
+ test => 'too-large remaining_time fails' },
+ { value => random_string(20),
+ error => 'is not a numeric value',
+ test => 'non-numeric remaining_time fails' },
+ # We use PRIVATE_BUG_USER because he can modify the bug, but
+ # can't change time-tracking fields.
+ { value => '100', user => PRIVATE_BUG_USER,
+ error => 'only a user with the required permissions',
+ test => 'non-timetracker can not set remaining_time' },
+ ],
+
+ # We do all the failing resolution tests on the second bug,
+ # because we want to be sure that we're starting from an open
+ # status.
+ resolution => [
+ { value => random_string(20), ids => [$second_id],
+ extra => { status => 'RESOLVED' },
+ error => 'There is no Resolution named',
+ test => 'invalid resolution fails' },
+ { value => 'FIXED', ids => [$second_id],
+ error => 'You cannot set a resolution for open bugs',
+ test => 'setting resolution on open bug fails' },
+ { value => 'DUPLICATE', ids => [$second_id],
+ extra => { status => 'RESOLVED' },
+ error => 'id to mark this bug as a duplicate',
+ test => 'setting DUPLICATE without dup_id fails' },
+ { value => '', ids => [$second_id],
+ extra => { status => 'RESOLVED' },
+ error => 'A valid resolution is required',
+ test => 'blank resolution fails with closed status' },
+ ],
+
+ see_also => [
+ { value => { add => [random_string(20)] },
+ error => 'is not a valid bug number nor an alias',
+ test => 'random string fails in see_also' },
+ { value => { add => ['http://landfill.bugzilla.org/'] },
+ error => 'See Also URLs should point to one of',
+ test => 'no show_bug.cgi in see_also URI' },
+ ],
+
+ status => [
+ { value => random_string(20),
+ error => 'There is no status named',
+ test => 'invalid status fails' },
+ { value => '',
+ error => 'You must select/enter a status',
+ test => 'blank status fails' },
+ # We use the second bug for this because we can guarantee that
+ # it is open.
+ { value => 'VERIFIED', ids => [$second_id],
+ extra => { resolution => 'FIXED' },
+ error => 'You are not allowed to change the bug status from',
+ test => 'invalid transition fails' },
+ ],
+
+ summary => [
+ { value => random_string(300),
+ error => 'The text you entered in the Summary field is too long',
+ test => 'too-long summary fails' },
+ { value => '',
+ error => 'You must enter a summary for this bug',
+ test => 'blank summary fails' },
+ ],
+
+ work_time => [
+ { value => 100_000_000,
+ error => 'more than the maximum allowable value',
+ test => 'too-large work_time fails' },
+ { value => random_string(20),
+ error => 'is not a numeric value',
+ test => 'non-numeric work_time fails' },
+ # We use PRIVATE_BUG_USER because he can modify the bug, but
+ # can't change time-tracking fields.
+ { value => '10', user => PRIVATE_BUG_USER,
+ error => 'only a user with the required permissions',
+ test => 'non-timetracker can not set work_time' },
+ ],
+ );
+
+ $values{depends_on} = $values{blocks};
+
+ foreach my $field (qw(platform priority severity target_milestone version))
+ {
+ my $tests = dclone($values{op_sys});
+ foreach my $test (@$tests) {
+ $test->{test} =~ s/op_sys/$field/g;
+ }
+ $values{$field} = $tests;
+ }
+
+ return %values;
+}
+
+sub invalid_values_to_tests {
+ my ($invalid_values, $public_bug) = @_;
+
+ my @tests;
+ foreach my $field (sort keys %$invalid_values) {
+ my @tests_invalid = @{ $invalid_values->{$field} };
+ foreach my $item (@tests_invalid) {
+ my %args = (
+ ids => $item->{ids} || [$public_bug->{id}],
+ $field => $item->{value},
+ %{ $item->{extra} || {} },
+ );
+ push(@tests, { user => $item->{user} || 'editbugs',
+ args => \%args,
+ error => $item->{error},
+ test => $item->{test} });
+ }
+ }
+
+ return \@tests;
+}
+
+###############
+# Main Script #
+###############
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+$jsonrpc_get->bz_call_fail('Bug.update',
+ { ids => ['public_bug'] },
+ 'must use HTTP POST', 'update fails over GET');
+
+sub post_success {
+ my ($call, $t, $rpc) = @_;
+ return if $t->{no_changes};
+ my $field = $t->{field};
+ return if !$field;
+
+ my @bugs = @{ $call->result->{bugs} };
+ foreach my $bug (@bugs) {
+ if ($field =~ /^comment/) {
+ _check_comment($bug, $field, $t, $rpc);
+ }
+ else {
+ _check_changes($bug, $field, $t);
+ }
+ }
+}
+
+sub _check_changes {
+ my ($bug, $field, $t) = @_;
+
+ my $changes = $bug->{changes}->{$field};
+ ok(defined $changes, "$field was changed")
+ or diag Dumper($bug, $t);
+
+ my $new_value = $t->{added};
+ $new_value = $t->{args}->{$field} if !defined $new_value;
+ _test_value($changes->{added}, $new_value, $field, 'added');
+
+ if (defined $t->{removed}) {
+ _test_value($changes->{removed}, $t->{removed}, $field, 'removed');
+ }
+}
+
+sub _test_value {
+ my ($got, $expected, $field, $type) = @_;
+ if ($field eq 'estimated_time' or $field eq 'remaining_time') {
+ cmp_ok($got, '==', $expected, "$field: $type is correct");
+ }
+ else {
+ is($got, $expected, "$field: $type is correct");
+ }
+}
+
+sub _check_comment {
+ my ($bug, $field, $t, $rpc) = @_;
+ my $bug_id = $bug->{id};
+ my $call = $rpc->bz_call_success('Bug.comments', { ids => [$bug_id] });
+ my $comments = $call->result->{bugs}->{$bug_id}->{comments};
+
+ if ($field eq 'comment_is_private') {
+ my $first_private = $comments->[0]->{is_private};
+ my ($expected) = values %{ $t->{args}->{comment_is_private} };
+ cmp_ok($first_private, '==', $expected,
+ 'description privacy is correct');
+ }
+ else {
+ my $last_comment = $comments->[-1];
+ my $expected = $t->{args}->{comment}->{body};
+ is($last_comment->{text}, $expected, 'comment added correctly');
+ }
+
+}
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+ $rpc->bz_run_tests(tests => get_tests($config, $rpc),
+ method => 'Bug.update', post_success => \&post_success);
+}
diff --git a/xt/webservice/bug_update_see_also.t b/xt/webservice/bug_update_see_also.t
new file mode 100644
index 000000000..79c3b5ea8
--- /dev/null
+++ b/xt/webservice/bug_update_see_also.t
@@ -0,0 +1,86 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#################################################
+# Test for xmlrpc call to Bug.update_see_also() #
+#################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER STANDARD_BUG_TESTS);
+use Test::More tests => 117;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+my $bug_url = 'http://landfill.bugzilla.org/bugzilla-tip/show_bug.cgi?id=100';
+
+# update_see_also doesn't support logged-out users.
+my @tests = grep { $_->{user} } @{ STANDARD_BUG_TESTS() };
+foreach my $t (@tests) {
+ $t->{args}->{add} = $t->{args}->{remove} = [];
+}
+
+push(@tests, (
+ { user => 'unprivileged',
+ args => { ids => ['public_bug'], add => [$bug_url] },
+ error => 'only the assignee or reporter of the bug, or a user',
+ test => 'Unprivileged user cannot add a URL to a bug',
+ },
+
+ { user => 'admin',
+ args => { ids => ['public_bug'], add => ['asdfasdfasdf'] },
+ error => 'asdf',
+ test => 'Admin cannot add an invalid URL',
+ },
+ { user => 'admin',
+ args => { ids => ['public_bug'], remove => ['asdfasdfasdf'] },
+ test => 'Invalid URL silently ignored',
+ },
+
+ { user => 'admin',
+ args => { ids => ['public_bug'], add => [$bug_url] },
+ test => 'Admin can add a URL to a public bug',
+ },
+ { user => 'unprivileged',
+ args => { ids => ['public_bug'], remove => [$bug_url] },
+ error => 'only the assignee or reporter of the bug, or a user',
+ test => 'Unprivileged user cannot remove a URL from a bug',
+ },
+ { user => 'admin',
+ args => { ids => ['public_bug'], remove => [$bug_url] },
+ test => 'Admin can remove a URL from a public bug',
+ },
+
+ { user => PRIVATE_BUG_USER,
+ args => { ids => ['private_bug'], add => [$bug_url] },
+ test => PRIVATE_BUG_USER . ' can add a URL to a private bug',
+ },
+ { user => PRIVATE_BUG_USER,
+ args => { ids => ['private_bug'], remove => [$bug_url] },
+ test => PRIVATE_BUG_USER . ' can remove a URL from a private bug',
+ },
+
+));
+
+sub post_success {
+ my ($call, $t) = @_;
+ isa_ok($call->result->{changes}, 'HASH', "Changes");
+}
+
+$jsonrpc_get->bz_call_fail('Bug.update_see_also',
+ { ids => ['public_bug'], add => [$bug_url] },
+ 'must use HTTP POST', 'update_see_also fails over GET');
+
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+ $rpc->bz_run_tests(tests => \@tests, method => 'Bug.update_see_also',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/bugzilla.t b/xt/webservice/bugzilla.t
new file mode 100644
index 000000000..2ddb13092
--- /dev/null
+++ b/xt/webservice/bugzilla.t
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##################################################
+# Test for xmlrpc call functions in Bugzilla.pm #
+##################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 11 * 3;
+use QA::Util;
+my ($config, @clients) = get_rpc_clients();
+
+foreach my $rpc (@clients) {
+ my $vers_call = $rpc->bz_call_success('Bugzilla.version');
+ my $version = $vers_call->result->{version};
+ ok($version, "Bugzilla.version returns $version");
+
+ my $tz_call = $rpc->bz_call_success('Bugzilla.timezone');
+ my $tz = $tz_call->result->{timezone};
+ ok($tz, "Bugzilla.timezone retuns $tz");
+
+ my $ext_call = $rpc->bz_call_success('Bugzilla.extensions');
+ my $extensions = $ext_call->result->{extensions};
+ isa_ok($extensions, 'HASH', 'extensions');
+
+ # There is always at least the QA extension enabled.
+ my $cmp = $config->{test_extensions} ? '>' : '==';
+ my @ext_names = keys %$extensions;
+ my $desc = scalar(@ext_names) . ' extension(s) returned: ' . join(', ', @ext_names);
+ cmp_ok(scalar(@ext_names), $cmp, 1, $desc);
+ ok(grep($_ eq 'QA', @ext_names), 'The QA extension is enabled');
+
+ my $time_call = $rpc->bz_call_success('Bugzilla.time');
+ my $time_result = $time_call->result;
+ foreach my $type (qw(db_time web_time)) {
+ cmp_ok($time_result->{$type}, '=~', $rpc->DATETIME_REGEX,
+ "Bugzilla.time returns a datetime for $type");
+ }
+}
diff --git a/xt/webservice/group_create.t b/xt/webservice/group_create.t
new file mode 100644
index 000000000..e46546a31
--- /dev/null
+++ b/xt/webservice/group_create.t
@@ -0,0 +1,101 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##########################################
+# Test for xmlrpc call to Group.create() #
+##########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 77;
+use QA::Util;
+
+use constant DESCRIPTION => 'Group created by Group.create';
+
+sub post_success {
+ my $call = shift;
+ my $gid = $call->result->{id};
+ ok($gid, "Got a non-zero group ID: $gid");
+}
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+my @tests = (
+ { args => { name => random_string(20), description => DESCRIPTION },
+ error => 'You must log in',
+ test => 'Logged-out user cannot call Group.create',
+ },
+ { user => 'unprivileged',
+ args => { name => random_string(20), description => DESCRIPTION },
+ error => 'you are not authorized',
+ test => 'Unprivileged user cannot call Group.create',
+ },
+ { user => 'admin',
+ args => { description => DESCRIPTION },
+ error => 'You must enter a name',
+ test => 'Missing name to Group.create',
+ },
+ { user => 'admin',
+ args => { name => random_string(20) },
+ error => 'You must enter a description',
+ test => 'Missing description to Group.create',
+ },
+ { user => 'admin',
+ args => { name => '', description => DESCRIPTION },
+ error => 'You must enter a name',
+ test => 'Name to Group.create cannot be empty',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), description => '' },
+ error => 'You must enter a description',
+ test => 'Description to Group.create cannot be empty',
+ },
+ { user => 'admin',
+ args => { name => 'canconfirm', description => DESCRIPTION },
+ error => 'already exists',
+ test => 'Name to Group.create already exists',
+ },
+ { user => 'admin',
+ args => { name => 'caNConFIrm', description => DESCRIPTION },
+ error => 'already exists',
+ test => 'Name to Group.create already exists but with a different case',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), description => DESCRIPTION,
+ user_regexp => '\\'},
+ error => 'The regular expression you entered is invalid',
+ test => 'The regular expression passed to Group.create is invalid',
+ },
+);
+
+$jsonrpc_get->bz_call_fail('Group.create',
+ { name => random_string(20), description => 'Created with JSON-RPC via GET' },
+ 'must use HTTP POST', 'Group.create fails over GET');
+
+foreach my $rpc ($xmlrpc, $jsonrpc) {
+ # Tests which work must be called from here,
+ # to avoid creating twice the same group.
+ my @all_tests = (@tests,
+ { user => 'admin',
+ args => { name => random_string(20), description => DESCRIPTION },
+ test => 'Passing the name and description only works',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), description => DESCRIPTION,
+ user_regexp => '\@foo.com$', is_active => 1,
+ icon_url => 'http://www.bugzilla.org/favicon.ico' },
+ test => 'Passing all arguments works',
+ },
+ );
+ $rpc->bz_run_tests(tests => \@all_tests, method => 'Group.create',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/jsonp.t b/xt/webservice/jsonp.t
new file mode 100644
index 000000000..75a0c0cfb
--- /dev/null
+++ b/xt/webservice/jsonp.t
@@ -0,0 +1,34 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 85;
+use QA::Util;
+my $jsonrpc_get = QA::Util::get_jsonrpc_client('GET');
+
+my @chars = (0..9, 'A'..'Z', 'a'..'z', '_[].');
+
+our @tests = (
+ { args => { callback => join('', @chars) },
+ test => 'callback accepts all legal characters.' },
+);
+foreach my $char (qw(! ~ ` @ $ % ^ & * - + = { } ; : ' " < > / ? |),
+ '(', ')', '\\', '#', ',')
+{
+ push(@tests,
+ { args => { callback => "a$char" },
+ error => "as your 'callback' parameter",
+ test => "$char is not valid in callback" });
+}
+
+$jsonrpc_get->bz_run_tests(method => 'Bugzilla.version', tests => \@tests);
diff --git a/xt/webservice/product_create.t b/xt/webservice/product_create.t
new file mode 100644
index 000000000..0ca117c31
--- /dev/null
+++ b/xt/webservice/product_create.t
@@ -0,0 +1,167 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+############################################
+# Test for xmlrpc call to Product.create() #
+############################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 121;
+use QA::Util;
+
+use constant DESCRIPTION => 'Product created by Product.create';
+use constant PROD_VERSION => 'unspecified';
+
+sub post_success {
+ my ($call, $test, $self) = @_;
+ my $args = $test->{args};
+ my $prod_id = $call->result->{id};
+ ok($prod_id, "Got a non-zero product ID: $prod_id");
+
+ $call = $self->bz_call_success("Product.get", {ids => [$prod_id]});
+ my $product = $call->result->{products}->[0];
+ my $prod_name = $product->{name};
+ my $is_active = defined $args->{is_open} ? $args->{is_open} : 1;
+ ok($product->{is_active} == $is_active,
+ "Product $prod_name has the correct value for is_active/is_open: $is_active");
+ my $has_unco = defined $args->{has_unconfirmed} ? $args->{has_unconfirmed} : 1;
+ ok($product->{has_unconfirmed} == $has_unco,
+ "Product $prod_name has the correct value for has_unconfirmed: $has_unco");
+}
+
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+my @tests = (
+ { args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION },
+ error => 'You must log in',
+ test => 'Logged-out user cannot call Product.create',
+ },
+ { user => 'unprivileged',
+ args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION },
+ error => 'you are not authorized',
+ test => 'Unprivileged user cannot call Product.create',
+ },
+ { user => 'admin',
+ args => { version => PROD_VERSION, description => DESCRIPTION },
+ error => 'You must enter a name',
+ test => 'Missing name to Product.create',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION },
+ error => 'You must enter a description',
+ test => 'Missing description to Product.create',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), description => DESCRIPTION },
+ error => 'You must enter a valid version',
+ test => 'Missing version to Product.create',
+ },
+ { user => 'admin',
+ args => { name => '', version => PROD_VERSION, description => DESCRIPTION },
+ error => 'You must enter a name',
+ test => 'Name to Product.create cannot be empty',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION, description => '' },
+ error => 'You must enter a description',
+ test => 'Description to Product.create cannot be empty',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), version => '', description => DESCRIPTION },
+ error => 'You must enter a valid version',
+ test => 'Version to Product.create cannot be empty',
+ },
+ { user => 'admin',
+ args => { name => random_string(20000), version => PROD_VERSION,
+ description => DESCRIPTION },
+ error => 'The name of a product is limited',
+ test => 'Name to Product.create too long',
+ },
+ { user => 'admin',
+ args => { name => 'Another Product', version => PROD_VERSION,
+ description => DESCRIPTION },
+ error => 'already exists',
+ test => 'Name to Product.create already exists',
+ },
+ { user => 'admin',
+ args => { name => 'aNoThEr Product', version => PROD_VERSION,
+ description => DESCRIPTION },
+ error => 'differs from existing product',
+ test => 'Name to Product.create already exists but with a different case',
+ },
+);
+
+# FIXME - Should be: if (classifications enabled).
+# But there is currently now way to query the value of a parameter via WS.
+if (0) {
+ push(@tests,
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION, has_unconfirmed => 1,
+ classification => '', default_milestone => '2.0',
+ is_open => 1, create_series => 1 },
+ error => 'You must select/enter a classification',
+ test => 'Passing an empty classification to Product.create fails',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION, has_unconfirmed => 1,
+ classification => random_string(10), default_milestone => '2.0',
+ is_open => 1, create_series => 1 },
+ error => 'You must select/enter a classification',
+ test => 'Passing an invalid classification to Product.create fails',
+ },
+ )
+}
+
+$jsonrpc_get->bz_call_fail('Product.create',
+ { name => random_string(20), version => PROD_VERSION,
+ description => 'Created with JSON-RPC via GET' },
+ 'must use HTTP POST', 'Product.create fails over GET');
+
+foreach my $rpc ($xmlrpc, $jsonrpc) {
+ # Tests which work must be called from here,
+ # to avoid creating twice the same product.
+ my @all_tests = (@tests,
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION },
+ test => 'Passing the name, description and version only works',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION, has_unconfirmed => 1,
+ classification => 'Class2_QA', default_milestone => '2.0',
+ is_open => 1, create_series => 1 },
+ test => 'Passing all arguments works',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION, has_unconfirmed => 0,
+ classification => 'Class2_QA', default_milestone => '2.0',
+ is_open => 0, create_series => 0 },
+ test => 'Passing null values works',
+ },
+ { user => 'admin',
+ args => { name => random_string(20), version => PROD_VERSION,
+ description => DESCRIPTION, has_unconfirmed => 1,
+ classification => 'Class2_QA', default_milestone => '',
+ is_open => 1, create_series => 1 },
+ test => 'Passing an empty default milestone works (falls back to "---")',
+ },
+ );
+ $rpc->bz_run_tests(tests => \@all_tests, method => 'Product.create',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/product_get.t b/xt/webservice/product_get.t
new file mode 100644
index 000000000..5cc6022d5
--- /dev/null
+++ b/xt/webservice/product_get.t
@@ -0,0 +1,113 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+########################################
+# Test for xmlrpc calls to: #
+# Product.get_selectable_products() #
+# Product.get_enterable_products() #
+# Product.get_accessible_products() #
+# Product.get() #
+########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Test::More tests => 134;
+use QA::Util;
+my ($config, @clients) = get_rpc_clients();
+
+my $products = $clients[0]->bz_get_products();
+my $public = $products->{'Another Product'};
+my $private = $products->{'QA-Selenium-TEST'};
+my $no_entry = $products->{'QA Entry Only'};
+my $no_search = $products->{'QA Search Only'};
+
+my %id_map = reverse %$products;
+
+my $tests = {
+ 'QA_Selenium_TEST' => {
+ selectable => [$public, $private, $no_entry, $no_search],
+ enterable => [$public, $private, $no_entry, $no_search],
+ accessible => [$public, $private, $no_entry, $no_search],
+ },
+ 'unprivileged' => {
+ selectable => [$public, $no_entry],
+ not_selectable => $no_search,
+ enterable => [$public, $no_search],
+ not_enterable => $no_entry,
+ accessible => [$public, $no_entry, $no_search],
+ not_accessible => $private,
+ },
+ '' => {
+ selectable => [$public, $no_entry],
+ not_selectable => $no_search,
+ enterable => [$public, $no_search],
+ not_enterable => $no_entry,
+ accessible => [$public, $no_entry, $no_search],
+ not_accessible => $private,
+ },
+};
+
+foreach my $rpc (@clients) {
+ foreach my $user (keys %$tests) {
+ my @selectable = @{ $tests->{$user}->{selectable} };
+ my @enterable = @{ $tests->{$user}->{enterable} };
+ my @accessible = @{ $tests->{$user}->{accessible} };
+ my $not_selectable = $tests->{$user}->{not_selectable};
+ my $not_enterable = $tests->{$user}->{not_enterable};
+ my $not_accessible = $tests->{$user}->{not_accessible};
+
+ $rpc->bz_log_in($user) if $user;
+ $user ||= "Logged-out user";
+
+ my $select_call =
+ $rpc->bz_call_success('Product.get_selectable_products');
+ my $select_ids = $select_call->result->{ids};
+ foreach my $id (@selectable) {
+ ok(grep($_ == $id, @$select_ids),
+ "$user can select " . $id_map{$id});
+ }
+ if ($not_selectable) {
+ ok(!grep($_ == $not_selectable, @$select_ids),
+ "$user cannot select " . $id_map{$not_selectable});
+ }
+
+ my $enter_call =
+ $rpc->bz_call_success('Product.get_enterable_products');
+ my $enter_ids = $enter_call->result->{ids};
+ foreach my $id (@enterable) {
+ ok(grep($_ == $id, @$enter_ids), "$user can enter " . $id_map{$id});
+ }
+ if ($not_enterable) {
+ ok(!grep($_ == $not_enterable, @$enter_ids),
+ "$user cannot enter " . $id_map{$not_enterable});
+ }
+
+ my $access_call =
+ $rpc->bz_call_success('Product.get_accessible_products');
+ my $get_call = $rpc->bz_call_success('Product.get',
+ { ids => \@accessible });
+ my $products = $get_call->result->{products};
+ my $expected_count = scalar @accessible;
+ cmp_ok(scalar @$products, '==', $expected_count,
+ "Product.get gets all $expected_count accessible products"
+ . " for $user.");
+ if ($not_accessible) {
+ my $no_access_call = $rpc->bz_call_success(
+ 'Product.get', { ids => [$not_accessible] });
+ ok(!scalar @{ $no_access_call->result->{products} },
+ "$user gets 0 products when asking for "
+ . $id_map{$not_accessible});
+ }
+
+ $rpc->bz_call_success('User.logout') if $user ne "Logged-out user";
+ }
+}
diff --git a/xt/webservice/user_create.t b/xt/webservice/user_create.t
new file mode 100644
index 000000000..38b55e69a
--- /dev/null
+++ b/xt/webservice/user_create.t
@@ -0,0 +1,118 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#########################################
+# Test for xmlrpc call to User.Create() #
+#########################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use Test::More tests => 75;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+use constant NEW_PASSWORD => 'password';
+use constant NEW_FULLNAME => 'WebService Created User';
+
+use constant PASSWORD_TOO_SHORT => 'a';
+
+# These are the characters that are actually invalid per RFC.
+use constant INVALID_EMAIL => '()[]\;:,<>@webservice.test';
+
+sub new_login {
+ return 'created_' . random_string(@_) . '@webservice.test';
+}
+
+sub post_success {
+ my ($call) = @_;
+ ok($call->result->{id}, "Got a non-zero user id");
+}
+
+$jsonrpc_get->bz_call_fail('User.create',
+ { email => new_login(), full_name => NEW_FULLNAME,
+ password => '*' },
+ 'must use HTTP POST', 'User.create fails over GET');
+
+# We have to wrap @tests in the foreach, because we want a different
+# login for each user, separately for each RPC client. (You can't create
+# two users with the same username, and XML-RPC would otherwise try to
+# create the same users that JSON-RPC created.)
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+ my @tests = (
+ # Permissions checks
+ { args => { email => new_login(), full_name => NEW_FULLNAME,
+ password => NEW_PASSWORD },
+ error => "you are not authorized",
+ test => 'Logged-out user cannot call User.create',
+ },
+ { user => 'unprivileged',
+ args => { email => new_login(), full_name => NEW_FULLNAME,
+ password => NEW_PASSWORD },
+ error => "you are not authorized",
+ test => 'Unprivileged user cannot call User.create',
+ },
+
+ # Login name checks.
+ { user => 'admin',
+ args => { full_name => NEW_FULLNAME, password => NEW_PASSWORD },
+ error => "argument was not set",
+ test => 'Leaving out email argument fails',
+ },
+ { user => 'admin',
+ args => { email => '', full_name => NEW_FULLNAME,
+ password => NEW_PASSWORD },
+ error => "argument was not set",
+ test => "Passing an empty email argument fails",
+ },
+ { user => 'admin',
+ args => { email => INVALID_EMAIL, full_name => NEW_FULLNAME,
+ password => NEW_PASSWORD },
+ error => "didn't pass our syntax checking",
+ test => 'Invalid email address fails',
+ },
+ { user => 'admin',
+ args => { email => new_login(128), full_name => NEW_FULLNAME,
+ password => NEW_PASSWORD },
+ error => "didn't pass our syntax checking",
+ test => 'Too long (> 127 chars) email address fails',
+ },
+ { user => 'admin',
+ args => { email => $config->{unprivileged_user_login},
+ full_name => NEW_FULLNAME, password => NEW_PASSWORD },
+ error => "There is already an account",
+ test => 'Trying to use an existing login name fails',
+ },
+
+ { user => 'admin',
+ args => { email => new_login(), full_name => NEW_FULLNAME,
+ password => PASSWORD_TOO_SHORT },
+ error => 'password must be at least',
+ test => 'Password Too Short fails',
+ },
+ { user => 'admin',
+ args => { email => new_login(), full_name => NEW_FULLNAME,
+ password => NEW_PASSWORD },
+ test => 'Creating a user with all arguments and correct privileges',
+ },
+ { user => 'admin',
+ args => { email => new_login(), password => NEW_PASSWORD },
+ test => 'Leaving out fullname works',
+ },
+ { user => 'admin',
+ args => { email => new_login(), full_name => NEW_FULLNAME },
+ test => 'Leaving out password works',
+ },
+ );
+
+ $rpc->bz_run_tests(tests => \@tests, method => 'User.create',
+ post_success => \&post_success);
+}
diff --git a/xt/webservice/user_get.t b/xt/webservice/user_get.t
new file mode 100644
index 000000000..02cf00fe7
--- /dev/null
+++ b/xt/webservice/user_get.t
@@ -0,0 +1,222 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+######################################
+# Test for xmlrpc call to User.get() #
+######################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use QA::Tests qw(PRIVATE_BUG_USER);
+use Test::More tests => 330;
+our ($config, @clients) = get_rpc_clients();
+
+my $get_user = $config->{'unprivileged_user_login'};
+my $canconfirm_user = $config->{'canconfirm_user_login'};
+my $priv_user = $config->{PRIVATE_BUG_USER . '_user_login'};
+my $disabled = $config->{'disabled_user_login'};
+my $disabled_match = substr($disabled, 0, length($disabled) - 1);
+
+# These are the basic tests. There are tests for include_fields
+# and exclude_field below.
+
+my @tests = (
+ { args => { names => [$get_user] },
+ test => "Logged-out user can get unprivileged user by name"
+ },
+ { args => { match => [$get_user] },
+ test => 'Logged-out user cannot use the match argument',
+ error => 'Logged-out users cannot use',
+ },
+ { args => { ids => [1] },
+ test => 'Logged-out users cannot use the "ids" argument',
+ error => 'Logged-out users cannot use',
+ },
+
+ # match & names
+ { user => 'unprivileged',
+ args => { names => [$get_user] },
+ test => "Unprivileged user can get himself",
+ },
+ { user => 'unprivileged',
+ args => { match => [$get_user] },
+ test => 'Logged-in user can use the match argument',
+ },
+ { user => 'unprivileged',
+ args => { match => [$get_user], names => [$get_user] },
+ test => 'Specifying the same thing in "match" and "names"',
+ },
+
+ # include_disabled
+ { user => 'unprivileged',
+ args => { match => [$get_user, $disabled_match] },
+ test => 'Disabled users are not normally returned'
+ },
+ { user => 'unprivileged',
+ args => { match => [$disabled_match], include_disabled => 1 },
+ test => 'Specifying include_disabled returns disabled users'
+ },
+ { user => 'unprivileged',
+ args => { match => [$disabled] },
+ test => 'Full match on a disabled user returns that user',
+ },
+
+ # groups and group_ids
+ { args => { groups => ['QA-Selenium-TEST'] },
+ test => 'Specifying just groups fails',
+ error => 'one of the following parameters',
+ },
+ { args => { group_ids => [1] },
+ test => 'Specifying just group ids fails',
+ error => 'one of the following parameters',
+ },
+ { args => { names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST'] },
+ test => 'Limiting the return value to a group while being logged out fails',
+ error => 'The group you specified, QA-Selenium-TEST, is not valid here',
+ },
+ { user => 'unprivileged',
+ args => { names => [$get_user, $priv_user], groups => ['missing_group'] },
+ test => 'Limiting the return value to a group which does not exist fails',
+ error => 'The group you specified, missing_group, is not valid here',
+ },
+ { user => 'unprivileged',
+ args => { names => [$get_user, $priv_user], groups => ['QA-Selenium-TEST'] },
+ test => 'Limiting the return value to a group you do not belong to fails',
+ error => 'The group you specified, QA-Selenium-TEST, is not valid here',
+ },
+ { user => 'editbugs',
+ args => { names => [$get_user, $priv_user], groups => ['canconfirm', 'editbugs'] },
+ test => 'Limiting the return value to some groups you do not belong to fails',
+ error => 'The group you specified, canconfirm, is not valid here',
+ },
+ { user => 'admin',
+ args => { names => [$canconfirm_user], groups => ['canconfirm', 'editbugs'] },
+ test => 'Limiting the return value to groups you belong to',
+ },
+
+ # groups returned
+ { user => 'admin',
+ args => { names => [$get_user] },
+ test => 'Admin can get user',
+ },
+ { user => 'admin',
+ args => { names => [$canconfirm_user] },
+ test => 'Admin can get user',
+ },
+ { user => 'canconfirm',
+ args => { names => [$canconfirm_user] },
+ test => 'Privileged user can get himself',
+ },
+ { user => 'editbugs',
+ args => { names => [$canconfirm_user] },
+ test => 'Privileged user can get another user',
+ },
+);
+
+sub post_success {
+ my ($call, $t) = @_;
+
+ my $result = $call->result;
+ is(scalar @{ $result->{users} }, 1, "Got exactly one user");
+ my $item = $result->{users}->[0];
+ my $user = $t->{user} || '';
+
+ if ($user eq 'admin') {
+ ok(exists $item->{email} && exists $item->{can_login}
+ && exists $item->{email_enabled} && exists $item->{login_denied_text},
+ 'Admin correctly gets all user fields');
+ }
+ elsif ($user) {
+ ok(exists $item->{email} && exists $item->{can_login},
+ 'Logged-in user correctly gets email and can_login');
+ ok(!exists $item->{email_enabled}
+ && !exists $item->{login_denied_text},
+ "Non-admin user doesn't get email_enabled and login_denied_text");
+ }
+ else {
+ my @item_keys = sort keys %$item;
+ is_deeply(\@item_keys, ['id', 'name', 'real_name'],
+ 'Only id, name, and real_name are returned to logged-out users');
+ return;
+ }
+
+ my $username = $config->{"${user}_user_login"};
+ # FIXME We have no way to create a saved search or a saved report from
+ # the WebService, so we cannot test that the correct data is returned
+ # if the user is accessing his own account.
+ if ($username eq $item->{name}) {
+ ok(exists $item->{saved_searches} && exists $item->{saved_reports},
+ 'Users can get the list of saved searches and reports for their own account');
+ }
+ else {
+ ok(!exists $item->{saved_searches} && !exists $item->{saved_reports},
+ "Users cannot get the list of saved searches and reports from someone else's acccount");
+ }
+
+ my @groups = map { $_->{name} } @{$item->{groups}};
+ # Admins can see all groups a user belongs to (assuming they inherited
+ # membership for all groups). Same for a user querying his own account.
+ if ($username eq $item->{name} || $user eq 'admin') {
+ if ($username eq $get_user) {
+ ok(!scalar @groups, "The unprivileged user doesn't belong to any group");
+ }
+ elsif ($username eq $canconfirm_user) {
+ ok(grep($_ eq 'canconfirm', @groups), "Group 'canconfirm' returned");
+ }
+ }
+ else {
+ ok(!scalar @groups, "No groups are visible to users without bless privs");
+ }
+}
+
+foreach my $rpc (@clients) {
+ $rpc->bz_run_tests(tests => \@tests, method => 'User.get',
+ post_success => \&post_success);
+
+ #############################
+ # Include and Exclude Tests #
+ #############################
+
+ my $include_nothing = $rpc->bz_call_success('User.get', {
+ names => [$get_user], include_fields => ['asdfasdfsdf'],
+ }, 'User.get including only invalid fields');
+ is(scalar keys %{ $include_nothing->result->{users}->[0] }, 0,
+ 'No fields returned for user');
+
+ my $include_one = $rpc->bz_call_success('User.get', {
+ names => [$get_user], include_fields => ['id'],
+ }, 'User.get including only id');
+ is(scalar keys %{ $include_one->result->{users}->[0] }, 1,
+ 'Only one field returned for user');
+
+ my $exclude_none = $rpc->bz_call_success('User.get', {
+ names => [$get_user], exclude_fields => ['asdfasdfsdf'],
+ }, 'User.get excluding only invalid fields');
+ is(scalar keys %{ $exclude_none->result->{users}->[0] }, 3,
+ 'All fields returned for user');
+
+ my $exclude_one = $rpc->bz_call_success('User.get', {
+ names => [$get_user], exclude_fields => ['id'],
+ }, 'User.get excluding id');
+ is(scalar keys %{ $exclude_one->result->{users}->[0] }, 2,
+ 'Only two fields returned for user');
+
+ my $override = $rpc->bz_call_success('User.get', {
+ names => [$get_user], include_fields => ['id', 'name'],
+ exclude_fields => ['id']
+ }, 'User.get with both include and exclude');
+ is(scalar keys %{ $override->result->{users}->[0] }, 1,
+ 'Only one field returned');
+ ok(exists $override->result->{users}->[0]->{name},
+ '...and that field is the "name" field');
+}
diff --git a/xt/webservice/user_login_logout.t b/xt/webservice/user_login_logout.t
new file mode 100644
index 000000000..fd5f8ef6b
--- /dev/null
+++ b/xt/webservice/user_login_logout.t
@@ -0,0 +1,128 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+##########################################################
+# Test for xmlrpc call to User.login() and User.logout() #
+##########################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use Data::Dumper;
+use QA::Util;
+use Test::More tests => 119;
+my ($config, @clients) = get_rpc_clients();
+
+use constant INVALID_EMAIL => '@invalid_user@';
+
+my $user = $config->{unprivileged_user_login};
+my $pass = $config->{unprivileged_user_passwd};
+my $error = "The login or password you entered is not valid";
+
+my @tests = (
+ { user => 'unprivileged',
+ test => "Unprivileged user can log in successfully",
+ },
+
+ { args => { login => $user, password => '' },
+ error => $error,
+ test => "Empty password can't log in",
+ },
+ { args => { login => '', password => $pass },
+ error => $error,
+ test => "Empty login can't log in",
+ },
+ { args => { login => $user },
+ error => "requires a password argument",
+ test => "Undef password can't log in",
+ },
+ { args => { password => $pass },
+ error => "requires a login argument",
+ test => "Undef login can't log in",
+ },
+
+ { args => { login => INVALID_EMAIL, password => $pass },
+ error => $error,
+ test => "Invalid email can't log in",
+ },
+ { args => { login => $user, password => '*' },
+ error => $error,
+ test => "Invalid password can't log in",
+ },
+
+ { args => { login => $config->{disabled_user_login},
+ password => $config->{disabled_user_passwd} },
+ error => "!!This is the text!!",
+ test => "Can't log in with a disabled account",
+ },
+ { args => { login => $config->{disabled_user_login}, password => '*' },
+ error => $error,
+ test => "Logging in with invalid password doesn't show disabledtext",
+ },
+);
+
+sub _login_args {
+ my $args = shift;
+ my %fixed_args = %$args;
+ $fixed_args{Bugzilla_login} = delete $fixed_args{login};
+ $fixed_args{Bugzilla_password} = delete $fixed_args{password};
+ return \%fixed_args;
+}
+
+foreach my $rpc (@clients) {
+ if ($rpc->bz_get_mode) {
+ $rpc->bz_call_fail('User.logout', undef, 'must use HTTP POST',
+ 'User.logout fails when called via GET');
+ }
+
+ foreach my $t (@tests) {
+ if ($t->{user}) {
+ my $username = $config->{$t->{user} . '_user_login'};
+ my $password = $config->{$t->{user} . '_user_passwd'};
+
+ if ($rpc->bz_get_mode) {
+ $rpc->bz_call_fail('User.login',
+ { login => $username, password => $password },
+ 'must use HTTP POST', $t->{test} . ' (fails on GET)');
+ }
+ else {
+ $rpc->bz_log_in($t->{user});
+ ok($rpc->{_bz_credentials}->{token}, 'Login token returned');
+ $rpc->bz_call_success('User.logout');
+ }
+
+ if ($t->{error}) {
+ $rpc->bz_call_fail('Bugzilla.version',
+ { Bugzilla_login => $username,
+ Bugzilla_password => $password });
+ }
+ else {
+ $rpc->bz_call_success('Bugzilla.version',
+ { Bugzilla_login => $username,
+ Bugzilla_password => $password });
+ }
+ }
+ else {
+ # Under GET, there's no reason to have extra failing tests.
+ if (!$rpc->bz_get_mode) {
+ $rpc->bz_call_fail('User.login', $t->{args}, $t->{error},
+ $t->{test});
+ }
+ if (defined $t->{args}->{login}
+ and defined $t->{args}->{password})
+ {
+ my $fixed_args = _login_args($t->{args});
+ $rpc->bz_call_fail('Bugzilla.version', $fixed_args,
+ $t->{error}, "Bugzilla_login: " . $t->{test});
+ }
+ }
+ }
+}
diff --git a/xt/webservice/user_offer_account_by_email.t b/xt/webservice/user_offer_account_by_email.t
new file mode 100644
index 000000000..785932167
--- /dev/null
+++ b/xt/webservice/user_offer_account_by_email.t
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+#########################################################
+# Test for xmlrpc call to User.offer_account_by_email() #
+#########################################################
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../lib";
+
+use QA::Util;
+use Test::More tests => 29;
+my ($config, $xmlrpc, $jsonrpc, $jsonrpc_get) = get_rpc_clients();
+
+# These are the characters that are actually invalid per RFC.
+use constant INVALID_EMAIL => '()[]\;:,<>@webservice.test';
+
+sub new_login {
+ return 'requested_' . random_string() . '@webservice.test';
+}
+
+$jsonrpc_get->bz_call_fail('User.offer_account_by_email',
+ { email => new_login() },
+ 'must use HTTP POST', 'offer_account_by_email fails over GET');
+
+# Have to wrap @tests in the foreach so that new_login returns something
+# different each time.
+foreach my $rpc ($jsonrpc, $xmlrpc) {
+ my @tests = (
+ # Login name checks.
+ { args => { },
+ error => "argument was not set",
+ test => 'Leaving out email argument fails',
+ },
+ { args => { email => '' },
+ error => "argument was not set",
+ test => "Passing an empty email argument fails",
+ },
+ { args => { email => INVALID_EMAIL },
+ error => "didn't pass our syntax checking",
+ test => 'Invalid email address fails',
+ },
+ { args => { email => $config->{unprivileged_user_login} },
+ error => "There is already an account",
+ test => 'Trying to use an existing login name fails',
+ },
+
+ { args => { email => new_login() },
+ test => 'Valid, non-existing email passes.',
+ },
+ );
+
+ $rpc->bz_run_tests(tests => \@tests,
+ method => 'User.offer_account_by_email');
+}