summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2014-05-19 06:44:19 +0200
committerDylan William Hardison <dylan@hardison.net>2014-05-21 21:29:48 +0200
commite1aea961a9dd83d6d14b4e45cbf4a70b00fbe18c (patch)
tree1305d5d3e61639142801114bd728bb57e6b09ad0
parent3d93c5919c4286c036d2cd0986387cecfdfeae60 (diff)
downloadbugzilla-e1aea961a9dd83d6d14b4e45cbf4a70b00fbe18c.tar.gz
bugzilla-e1aea961a9dd83d6d14b4e45cbf4a70b00fbe18c.tar.xz
Bug 993223 - Notify Review Board when a bug is made confidential
r=glob
-rw-r--r--extensions/Push/Extension.pm5
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard.pm187
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard/Client.pm67
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard/Resource.pm38
-rw-r--r--extensions/Push/lib/Connector/ReviewBoard/ReviewRequest.pm28
-rw-r--r--extensions/Push/t/ReviewBoard.t224
6 files changed, 548 insertions, 1 deletions
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;