From e1aea961a9dd83d6d14b4e45cbf4a70b00fbe18c Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Mon, 19 May 2014 00:44:19 -0400 Subject: Bug 993223 - Notify Review Board when a bug is made confidential r=glob --- extensions/Push/Extension.pm | 5 +- extensions/Push/lib/Connector/ReviewBoard.pm | 187 +++++++++++++++++ .../Push/lib/Connector/ReviewBoard/Client.pm | 67 ++++++ .../Push/lib/Connector/ReviewBoard/Resource.pm | 38 ++++ .../lib/Connector/ReviewBoard/ReviewRequest.pm | 28 +++ extensions/Push/t/ReviewBoard.t | 224 +++++++++++++++++++++ 6 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 extensions/Push/lib/Connector/ReviewBoard.pm create mode 100644 extensions/Push/lib/Connector/ReviewBoard/Client.pm create mode 100644 extensions/Push/lib/Connector/ReviewBoard/Resource.pm create mode 100644 extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm create mode 100644 extensions/Push/t/ReviewBoard.t (limited to 'extensions/Push') diff --git a/extensions/Push/Extension.pm b/extensions/Push/Extension.pm index d38dcb032..3332df8e8 100644 --- a/extensions/Push/Extension.pm +++ b/extensions/Push/Extension.pm @@ -157,8 +157,11 @@ sub _object_modified { changes => [], }; foreach my $field_name (sort keys %$changes) { + my $new_field_name = $field_name; + $new_field_name =~ s/isprivate/is_private/; + push @{$changes_data->{'changes'}}, { - field => $field_name, + field => $new_field_name, removed => $changes->{$field_name}[0], added => $changes->{$field_name}[1], }; diff --git a/extensions/Push/lib/Connector/ReviewBoard.pm b/extensions/Push/lib/Connector/ReviewBoard.pm new file mode 100644 index 000000000..b5d1a9214 --- /dev/null +++ b/extensions/Push/lib/Connector/ReviewBoard.pm @@ -0,0 +1,187 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::Push::Connector::ReviewBoard; + +use strict; +use warnings; + +use base 'Bugzilla::Extension::Push::Connector::Base'; + +use Bugzilla::Constants; +use Bugzilla::Extension::Push::Constants; +use Bugzilla::Extension::Push::Util; +use Bugzilla::Bug; +use Bugzilla::Attachment; +use Bugzilla::Extension::Push::Connector::ReviewBoard::Client; + +use JSON 'decode_json'; +use DateTime; +use Scalar::Util 'blessed'; + +use constant RB_CONTENT_TYPE => 'text/x-review-board-request'; + +sub client { + my $self = shift; + + $self->{client} //= Bugzilla::Extension::Push::Connector::ReviewBoard::Client->new( + base_uri => $self->config->{base_uri}, + username => $self->config->{username}, + password => $self->config->{password}, + $self->config->{proxy} ? (proxy => $self->config->{proxy}) : (), + ); + + return $self->{client}; +} + +sub options { + return ( + { + name => 'base_uri', + label => 'Base URI for ReviewBoard', + type => 'string', + default => 'https://reviewboard.allizom.org', + required => 1, + }, + { + name => 'username', + label => 'Username', + type => 'string', + default => 'guest', + required => 1, + }, + { + name => 'password', + label => 'Password', + type => 'password', + default => 'guest', + required => 1, + }, + { + name => 'proxy', + label => 'Proxy', + type => 'string', + }, + ); +} + +sub stop { + my ($self) = @_; +} + +sub should_send { + my ($self, $message) = @_; + + if ($message->routing_key =~ /^(?:attachment|bug)\.modify:.*\bis_private\b/) { + my $payload = $message->payload_decoded(); + my $target = $payload->{event}->{target}; + + if ($target ne 'bug' && exists $payload->{$target}->{bug}) { + return 0 if $payload->{$target}->{bug}->{is_private}; + return 0 if $payload->{$target}->{content_type} ne RB_CONTENT_TYPE; + } + + return $payload->{$target}->{is_private} ? 1 : 0; + } + else { + # We're not interested in the message. + return 0; + } +} + +sub send { + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + + eval { + my $payload = $message->payload_decoded(); + my $target = $payload->{event}->{target}; + + if (my $method = $self->can("_process_$target")) { + $self->$method($payload->{$target}); + } + }; + if ($@) { + return (PUSH_RESULT_TRANSIENT, clean_error($@)); + } + + return PUSH_RESULT_OK; +} + +sub _process_attachment { + my ($self, $payload_target) = @_; + my $logger = Bugzilla->push_ext->logger; + my $attachment = blessed($payload_target) + ? $payload_target + : Bugzilla::Attachment->new({ id => $payload_target->{id}, cache => 1 }); + + if ($attachment) { + my $content = $attachment->data; + my $base_uri = quotemeta($self->config->{base_uri}); + if (my ($id) = $content =~ m|$base_uri/r/([0-9]+)|) { + my $resp = $self->client->review_request->delete($id); + my $content = $resp->decoded_content; + my $status = $resp->code; + my $result = $content && decode_json($content) ; + + if ($status == 204) { + # Success, review request deleted! + $logger->debug("Deleted review request $id"); + } + elsif ($status == 404) { + # API error 100 - Does Not Exist + $logger->debug("Does Not Exist: Review Request $id does not exist"); + } + elsif ($status == 403) { + # API error 101 - Permission Denied + $logger->error("Permission Denied: ReviewBoard Push Connector may be misconfigured"); + die $result->{err}{msg}; + } + elsif ($status == 401) { + # API error 103 - Not logged in + $logger->error("Not logged in: ReviewBoard Push Connector may be misconfigured"); + die $result->{err}{msg}; + } + else { + if ($result) { + my $code = $result->{err}{code}; + my $msg = $result->{err}{msg}; + $logger->error("Unexpected API Error: ($code) $msg"); + die $msg; + } + else { + $logger->error("Unexpected HTTP Response $status"); + die "HTTP Status: $status"; + } + } + } + else { + $logger->error("Cannot find link: ReviewBoard Push Connector may be misconfigured"); + die "Unable to find link in $content"; + } + } + else { + $logger->error("Cannot find attachment with id = $payload_target->{id}"); + } +} + +sub _process_bug { + my ($self, $payload_target) = @_; + + Bugzilla->set_user(Bugzilla::User->super_user); + my $bug = Bugzilla::Bug->new({ id => $payload_target->{id}, cache => 1 }); + my @attachments = @{ $bug->attachments }; + Bugzilla->logout; + + foreach my $attachment (@attachments) { + next if $attachment->contenttype ne RB_CONTENT_TYPE; + $self->_process_attachment($attachment); + } +} + +1; diff --git a/extensions/Push/lib/Connector/ReviewBoard/Client.pm b/extensions/Push/lib/Connector/ReviewBoard/Client.pm new file mode 100644 index 000000000..7ec4938d2 --- /dev/null +++ b/extensions/Push/lib/Connector/ReviewBoard/Client.pm @@ -0,0 +1,67 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::Push::Connector::ReviewBoard::Client; + +use 5.10.1; +use strict; +use warnings; + +use Carp qw(croak); +use LWP::UserAgent; +use Scalar::Util qw(blessed); +use URI; + +use Bugzilla::Extension::Push::Connector::ReviewBoard::ReviewRequest; + +sub new { + my ($class, %params) = @_; + + croak "->new() is a class method" if blessed($class); + return bless(\%params, $class); +} + +sub username { $_[0]->{username} } +sub password { $_[0]->{password} } +sub base_uri { $_[0]->{base_uri} } +sub realm { $_[0]->{realm} // 'Web API' } +sub proxy { $_[0]->{proxy} } + +sub _netloc { + my $self = shift; + + my $uri = URI->new($self->base_uri); + return $uri->host . ':' . $uri->port; +} + +sub useragent { + my $self = shift; + + unless ($self->{useragent}) { + my $ua = LWP::UserAgent->new(agent => Bugzilla->params->{urlbase}); + $ua->credentials( + $self->_netloc, + $self->realm, + $self->username, + $self->password, + ); + $ua->proxy('https', $self->proxy) if $self->proxy; + $ua->timeout(10); + + $self->{useragent} = $ua; + } + + return $self->{useragent}; +} + +sub review_request { + my $self = shift; + + return Bugzilla::Extension::Push::Connector::ReviewBoard::ReviewRequest->new(client => $self, @_); +} + +1; diff --git a/extensions/Push/lib/Connector/ReviewBoard/Resource.pm b/extensions/Push/lib/Connector/ReviewBoard/Resource.pm new file mode 100644 index 000000000..3f8d434ce --- /dev/null +++ b/extensions/Push/lib/Connector/ReviewBoard/Resource.pm @@ -0,0 +1,38 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::Push::Connector::ReviewBoard::Resource; + +use 5.10.1; +use strict; +use warnings; + +use URI; +use Carp qw(croak confess); +use Scalar::Util qw(blessed); + +sub new { + my ($class, %params) = @_; + + croak "->new() is a class method" if blessed($class); + return bless(\%params, $class); +} + +sub client { $_[0]->{client} } + +sub path { confess 'Unimplemented'; } + +sub uri { + my ($self, @path) = @_; + + my $uri = URI->new($self->client->base_uri); + $uri->path(join('/', $self->path, @path) . '/'); + + return $uri; +} + +1; diff --git a/extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm b/extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm new file mode 100644 index 000000000..32bebfbe8 --- /dev/null +++ b/extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::Push::Connector::ReviewBoard::ReviewRequest; + +use 5.10.1; +use strict; +use warnings; + +use base 'Bugzilla::Extension::Push::Connector::ReviewBoard::Resource'; + +# Reference: http://www.reviewboard.org/docs/manual/dev/webapi/2.0/resources/review-request/ + +sub path { + return '/api/review-requests'; +} + +sub delete { + my ($self, $id) = @_; + + return $self->client->useragent->delete($self->uri($id)); +} + +1; diff --git a/extensions/Push/t/ReviewBoard.t b/extensions/Push/t/ReviewBoard.t new file mode 100644 index 000000000..f2a508f59 --- /dev/null +++ b/extensions/Push/t/ReviewBoard.t @@ -0,0 +1,224 @@ +#!/usr/bin/perl -T +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. +use strict; +use warnings; +use lib qw( . lib ); + +use Test::More; +use Bugzilla; +use Bugzilla::Extension; +use Bugzilla::Attachment; +use Scalar::Util 'blessed'; +use YAML; + +BEGIN { + eval { + require Test::LWP::UserAgent; + require Test::MockObject; + }; + if ($@) { + plan skip_all => + 'Tests require Test::LWP::UserAgent and Test::MockObject'; + exit; + } +} + +BEGIN { + Bugzilla->extensions; # load all of them + use_ok 'Bugzilla::Extension::Push::Connector::ReviewBoard::Client'; + use_ok 'Bugzilla::Extension::Push::Constants'; +} + +my ($push) = grep { blessed($_) eq 'Bugzilla::Extension::Push' } @{Bugzilla->extensions }; +my $connectors = $push->_get_instance->connectors; +my $con = $connectors->by_name('ReviewBoard'); + +my $ua_204 = Test::LWP::UserAgent->new; +$ua_204->map_response( + qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+}, + HTTP::Response->new('204')); + +my $ua_404 = Test::LWP::UserAgent->new; +$ua_404->map_response( + qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+}, + HTTP::Response->new('404', undef, undef, q[{ "err": { "code": 100, "msg": "Object does not exist" }, "stat": "fail" }])); + +# forbidden +my $ua_403 = Test::LWP::UserAgent->new; +$ua_403->map_response( + qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+}, + HTTP::Response->new('403', undef, undef, q[ {"err":{"code":101,"msg":"You don't have permission for this"},"stat":"fail"}])); + +# not logged in +my $ua_401 = Test::LWP::UserAgent->new; +$ua_401->map_response( + qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+}, + HTTP::Response->new('401', undef, undef, q[ { "err": { "code": 103, "msg": "You are not logged in" }, "stat": "fail" } ])); + +# not logged in +my $ua_500 = Test::LWP::UserAgent->new; +$ua_500->map_response( + qr{https://reviewboard-dev\.allizom\.org/api/review-requests/\d+}, + HTTP::Response->new('500')); + +$con->client->{useragent} = $ua_204; +$con->config->{base_uri} = 'https://reviewboard-dev.allizom.org'; +$con->client->{base_uri} = 'https://reviewboard-dev.allizom.org'; + +{ + my $msg = message( + event => { + routing_key => 'attachment.modify:is_private', + target => 'attachment', + }, + attachment => { + is_private => 1, + content_type => 'text/plain', + bug => { id => 1, is_private => 0 }, + }, + ); + + ok(not($con->should_send($msg)), "text/plain message should not be sent"); +} + +my $data = slurp("extensions/Push/t/rblink.txt"); +Bugzilla::User::DEFAULT_USER->{userid} = 42; +Bugzilla->set_user(Bugzilla::User->super_user); +diag " " . Bugzilla::User->super_user->id; + +my $dbh = Bugzilla->dbh; +$dbh->bz_start_transaction; +my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +my $bug = Bugzilla::Bug->new({id => 9000}); +my $attachment = Bugzilla::Attachment->create( + { bug => $bug, + creation_ts => $timestamp, + data => $data, + filesize => length $data, + description => "rblink.txt", + filename => "rblink.txt", + isprivate => 1, ispatch => 0, + mimetype => 'text/x-review-board-request'}); +diag "".$attachment->id; +$dbh->bz_commit_transaction; + +{ + my $msg = message( + event => { + routing_key => 'attachment.modify:cc,is_private', + target => 'attachment', + }, + attachment => { + id => $attachment->id, + is_private => 1, + content_type => 'text/x-review-board-request', + bug => { id => $bug->id, is_private => 0 }, + }, + ); + ok($con->should_send($msg), "rb attachment should be sent"); + + { + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_OK, "good push result"); + diag $err if $err; + } + + { + local $con->client->{useragent} = $ua_404; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_OK, "good push result for 404"); + diag $err if $err; + } + + + { + local $con->client->{useragent} = $ua_403; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_TRANSIENT, "transient error on 403"); + diag $err if $err; + } + + + { + local $con->client->{useragent} = $ua_401; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_TRANSIENT, "transient error on 401"); + diag $err if $err; + } + + { + local $con->client->{useragent} = $ua_500; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_TRANSIENT, "transient error on 500"); + diag $err if $err; + } +} + +{ + my $msg = message( + event => { + routing_key => 'bug.modify:is_private', + target => 'bug', + }, + bug => { + is_private => 1, + id => $bug->id, + }, + ); + + ok($con->should_send($msg), "rb attachment should be sent"); + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_OK, "good push result"); + + { + local $con->client->{useragent} = $ua_404; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_OK, "good push result for 404"); + } + + { + local $con->client->{useragent} = $ua_403; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_TRANSIENT, "transient error on 404"); + diag $err if $err; + } + + + { + local $con->client->{useragent} = $ua_401; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_TRANSIENT, "transient error on 401"); + diag $err if $err; + } + + { + local $con->client->{useragent} = $ua_401; + my ($rv, $err) = $con->send($msg); + is($rv, PUSH_RESULT_TRANSIENT, "transient error on 401"); + diag $err if $err; + } +} + +sub message { + my $msg_data = { @_ }; + + return Test::MockObject->new + ->set_always( routing_key => $msg_data->{event}{routing_key} ) + ->set_always( payload_decoded => $msg_data ); +} + +sub slurp { + my $file = shift; + local $/ = undef; + open my $fh, '<', $file or die "unable to open $file"; + my $s = readline $fh; + close $fh; + return $s; +} + +done_testing; -- cgit v1.2.3-24-g4f1b