# 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); }