[Bps-public-commit] rt-extension-rest2 branch, update-custom-roles-on-correspond-and-comment, created. 1.09-13-gd20d560

Jim Brandt jbrandt at bestpractical.com
Mon Jan 25 15:17:22 EST 2021


The branch, update-custom-roles-on-correspond-and-comment has been created
        at  d20d5605523b6ea76c3a04d4833ab7f5ba4975d0 (commit)

- Log -----------------------------------------------------------------
commit d3b553455ad50271ee85e6428eaac0485eb4cccd
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Fri Jan 15 13:57:49 2021 -0500

    Move update_role_members to Util for general use
    
    Like custom field updates previously, custom role
    updates can happen in multiple places. Move
    update_role_members so they can all call the same code.
    
    Also implement fix_custom_role_ids in Util.pm to standardize
    different types of input parameters for custom roles.

diff --git a/lib/RT/Extension/REST2/Resource/Record/Writable.pm b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
index 3a4b763..d8e7068 100644
--- a/lib/RT/Extension/REST2/Resource/Record/Writable.pm
+++ b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
@@ -5,7 +5,7 @@ use warnings;
 use Moose::Role;
 use namespace::autoclean;
 use JSON ();
-use RT::Extension::REST2::Util qw( deserialize_record error_as_json expand_uid update_custom_fields );
+use RT::Extension::REST2::Util qw( deserialize_record error_as_json expand_uid update_custom_fields update_role_members);
 use List::MoreUtils 'uniq';
 
 with 'RT::Extension::REST2::Resource::Role::RequestBodyIsJSON'
@@ -125,7 +125,7 @@ sub update_record {
     );
 
     push @results, update_custom_fields($self->record, $data->{CustomFields});
-    push @results, $self->_update_role_members($data);
+    push @results, update_role_members($self->record, $data);
     push @results, $self->_update_disabled($data->{Disabled})
       unless grep { $_ eq 'Disabled' } $self->record->WritableAttributes;
     push @results, $self->_update_privileged($data->{Privileged})
@@ -136,124 +136,6 @@ sub update_record {
     return @results;
 }
 
-sub _update_role_members {
-    my $self = shift;
-    my $data = shift;
-
-    my $record = $self->record;
-
-    return unless $record->DOES('RT::Record::Role::Roles');
-
-    my @results;
-
-    foreach my $role ($record->Roles) {
-        next unless exists $data->{$role};
-
-        # special case: RT::Ticket->Update already handles Owner for us
-        next if $role eq 'Owner' && $record->isa('RT::Ticket');
-
-        my $val = $data->{$role};
-
-        if ($record->Role($role)->{Single}) {
-            if (ref($val) eq 'ARRAY') {
-                $val = $val->[0];
-            }
-            elsif (ref($val)) {
-                die "Invalid value type for role $role";
-            }
-
-            my ($ok, $msg);
-            if ($record->can('AddWatcher')) {
-                ($ok, $msg) = $record->AddWatcher(
-                    Type => $role,
-                    User => $val,
-                );
-            } else {
-                ($ok, $msg) = $record->AddRoleMember(
-                    Type => $role,
-                    User => $val,
-                );
-            }
-            push @results, $msg;
-        }
-        else {
-            my %count;
-            my @vals;
-
-            for (ref($val) eq 'ARRAY' ? @$val : $val) {
-                my ($principal_id, $msg);
-
-                if (/^\d+$/) {
-                    $principal_id = $_;
-                }
-                elsif ($record->can('CanonicalizePrincipal')) {
-                    ((my $principal), $msg) = $record->CanonicalizePrincipal(User => $_);
-                    $principal_id = $principal->Id;
-                }
-                else {
-                    my $user = RT::User->new($record->CurrentUser);
-                    if (/@/) {
-                        ((my $ok), $msg) = $user->LoadOrCreateByEmail( $_ );
-                    } else {
-                        ((my $ok), $msg) = $user->Load( $_ );
-                    }
-                    $principal_id = $user->PrincipalId;
-                }
-
-                if (!$principal_id) {
-                    push @results, $msg;
-                    next;
-                }
-
-                push @vals, $principal_id;
-                $count{$principal_id}++;
-            }
-
-            my $group = $record->RoleGroup($role);
-            my $members = $group->MembersObj;
-            while (my $member = $members->Next) {
-                $count{$member->MemberId}--;
-            }
-
-            # RT::Ticket has specialized methods
-            my $add_method = $record->can('AddWatcher') ? 'AddWatcher' : 'AddRoleMember';
-            my $del_method = $record->can('DeleteWatcher') ? 'DeleteWatcher' : 'DeleteRoleMember';
-
-            # we want to provide a stable order, so first go by the order
-            # provided in the argument list, and then for any role members
-            # that are being removed, remove in sorted order
-            for my $id (uniq(@vals, sort keys %count)) {
-                my $count = $count{$id};
-                if ($count == 0) {
-                    # new == old, no change needed
-                }
-                elsif ($count > 0) {
-                    # new > old, need to add new
-                    while ($count-- > 0) {
-                        my ($ok, $msg) = $record->$add_method(
-                            Type        => $role,
-                            PrincipalId => $id,
-                        );
-                        push @results, $msg;
-                    }
-                }
-                elsif ($count < 0) {
-                    # old > new, need to remove old
-                    while ($count++ < 0) {
-                        my ($ok, $msg) = $record->$del_method(
-                            Type        => $role,
-                            PrincipalId => $id,
-                        );
-                        push @results, $msg;
-                    }
-                }
-            }
-        }
-    }
-
-    return @results;
-}
-
 sub _update_disabled {
     my $self = shift;
     my $data = shift;
diff --git a/lib/RT/Extension/REST2/Util.pm b/lib/RT/Extension/REST2/Util.pm
index 6a2ba19..55e05f8 100644
--- a/lib/RT/Extension/REST2/Util.pm
+++ b/lib/RT/Extension/REST2/Util.pm
@@ -21,6 +21,8 @@ use Sub::Exporter -setup => {
         custom_fields_for
         format_datetime
         update_custom_fields
+        update_role_members
+        fix_custom_role_ids
     ]]
 };
 
@@ -396,5 +398,154 @@ sub update_custom_fields {
     return @results;
 }
 
+sub update_role_members {
+    my $record = shift;
+    my $data = shift;
+
+    return unless $record->DOES('RT::Record::Role::Roles');
+
+    my @results;
+
+    foreach my $role ($record->Roles) {
+        next unless exists $data->{$role};
+
+        # special case: RT::Ticket->Update already handles Owner for us
+        next if $role eq 'Owner' && $record->isa('RT::Ticket');
+
+        my $val = $data->{$role};
+
+        if ($record->Role($role)->{Single}) {
+            if (ref($val) eq 'ARRAY') {
+                $val = $val->[0];
+            }
+            elsif (ref($val)) {
+                die "Invalid value type for role $role";
+            }
+
+            my ($ok, $msg);
+            if ($record->can('AddWatcher')) {
+                ($ok, $msg) = $record->AddWatcher(
+                    Type => $role,
+                    User => $val,
+                );
+            } else {
+                ($ok, $msg) = $record->AddRoleMember(
+                    Type => $role,
+                    User => $val,
+                );
+            }
+            push @results, $msg;
+        }
+        else {
+            my %count;
+            my @vals;
+
+            for (ref($val) eq 'ARRAY' ? @$val : $val) {
+                my ($principal_id, $msg);
+
+                if (/^\d+$/) {
+                    $principal_id = $_;
+                }
+                elsif ($record->can('CanonicalizePrincipal')) {
+                    ((my $principal), $msg) = $record->CanonicalizePrincipal(User => $_);
+                    $principal_id = $principal->Id;
+                }
+                else {
+                    my $user = RT::User->new($record->CurrentUser);
+                    if (/@/) {
+                        ((my $ok), $msg) = $user->LoadOrCreateByEmail( $_ );
+                    } else {
+                        ((my $ok), $msg) = $user->Load( $_ );
+                    }
+                    $principal_id = $user->PrincipalId;
+                }
+
+                if (!$principal_id) {
+                    push @results, $msg;
+                    next;
+                }
+
+                push @vals, $principal_id;
+                $count{$principal_id}++;
+            }
+
+            my $group = $record->RoleGroup($role);
+            my $members = $group->MembersObj;
+            while (my $member = $members->Next) {
+                $count{$member->MemberId}--;
+            }
+
+            # RT::Ticket has specialized methods
+            my $add_method = $record->can('AddWatcher') ? 'AddWatcher' : 'AddRoleMember';
+            my $del_method = $record->can('DeleteWatcher') ? 'DeleteWatcher' : 'DeleteRoleMember';
+
+            # we want to provide a stable order, so first go by the order
+            # provided in the argument list, and then for any role members
+            # that are being removed, remove in sorted order
+            for my $id (uniq(@vals, sort keys %count)) {
+                my $count = $count{$id};
+                if ($count == 0) {
+                    # new == old, no change needed
+                }
+                elsif ($count > 0) {
+                    # new > old, need to add new
+                    while ($count-- > 0) {
+                        my ($ok, $msg) = $record->$add_method(
+                            Type        => $role,
+                            PrincipalId => $id,
+                        );
+                        push @results, $msg;
+                    }
+                }
+                elsif ($count < 0) {
+                    # old > new, need to remove old
+                    while ($count++ < 0) {
+                        my ($ok, $msg) = $record->$del_method(
+                            Type        => $role,
+                            PrincipalId => $id,
+                        );
+                        push @results, $msg;
+                    }
+                }
+            }
+        }
+    }
+
+    return @results;
+}
+
+=head2 fix_custom_role_ids ( $record, $custom_roles )
+
+$record is the RT object (eg, an RT::Ticket) associated
+with custom roles.
+
+$custom_roles is a hashref where the keys are custom role
+IDs, names or email addresses and the values can be
+anything.  Returns a new hashref where all the keys
+are replaced with "RT::CustomRole-ID" if they were
+not originally in that form, and the values are kept
+the same.
+
+=cut
+
+sub fix_custom_role_ids
+{
+    my ($record, $custom_roles) = @_;
+    my $ret = {};
+    return $ret unless $custom_roles;
+
+    foreach my $key (keys(%$custom_roles)) {
+        if ($key =~ /^RT::CustomRole-\d+$/) {
+            # Already in the correct form
+            $ret->{$key} = $custom_roles->{$key};
+            next;
+        }
+
+        my $cr = RT::CustomRole->new($record->CurrentUser);
+        next unless $cr->Load($key);
+        $ret->{'RT::CustomRole-' . $cr->Id} = $custom_roles->{$key};
+    }
+    return $ret;
+}
 
 1;

commit 5054bf020357014efb30807008648e1c9266ebae
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Fri Jan 15 13:59:05 2021 -0500

    Allow updating of custom roles on ticket comment/correspond

diff --git a/lib/RT/Extension/REST2/Resource/Message.pm b/lib/RT/Extension/REST2/Resource/Message.pm
index f15d996..2d39e33 100644
--- a/lib/RT/Extension/REST2/Resource/Message.pm
+++ b/lib/RT/Extension/REST2/Resource/Message.pm
@@ -7,7 +7,7 @@ use namespace::autoclean;
 use MIME::Base64;
 
 extends 'RT::Extension::REST2::Resource';
-use RT::Extension::REST2::Util qw( error_as_json update_custom_fields );
+use RT::Extension::REST2::Util qw( error_as_json update_custom_fields update_role_members fix_custom_role_ids);
 
 sub dispatch_rules {
     Path::Dispatcher::Rule::Regex->new(
@@ -150,6 +150,11 @@ sub add_message {
 
     push @results, $msg;
     push @results, update_custom_fields($self->record, $args{CustomFields});
+
+    # update_role_members wants custom role IDs (like RT::CustomRole-ID)
+    # rather than role names.
+    my $renamed_custom_roles = fix_custom_role_ids($self->record, $args{CustomRoles});
+    push @results, update_role_members($self->record, $renamed_custom_roles);
     push @results, $self->_update_txn_custom_fields( $TransObj, $args{TxnCustomFields} || $args{TransactionCustomFields} );
 
     # Set ticket status if we were passed a "Status":"foo" argument

commit 608e4f79011b4301f56c2e43e93931e330eba44d
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Fri Jan 15 14:01:05 2021 -0500

    Document CustomFields, TxnCustomFields and CustomRoles data members

diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index e11acfb..3416ad5 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -275,6 +275,31 @@ The new status (for example, "open", "rejected", etc.) to set the
 ticket to.  The Status value must be a valid status based on the
 lifecycle of the ticket's current queue.
 
+=item C<CustomRoles>
+
+A hash whose keys are custom role names and values are as described below:
+
+For a single-value custom role, the value must be a string representing an
+email address or user name; the custom role is set to the user with
+that email address or user name.
+
+For a multi-value custom role, the value can be a string representing
+an email address or user name, or can be an array of email addresses
+or user names; in either case, the members of the custom role are set
+to the corresponding users.
+
+=item C<CustomFields>
+
+A hash similar to the C<CustomRoles> hash, but whose keys are custom
+field names that apply to the Ticket; those fields are set to the
+supplied values.
+
+=item C<TxnCustomFields>
+
+A hash similar to the C<CustomRoles> hash, but whose keys are custom
+field names that apply to the Transaction; those fields are set
+to the supplied values.
+
 =back
 
 =head3 Add Attachments
@@ -505,6 +530,11 @@ Below are some examples using the endpoints above.
         -d '{ "Content": "Testing a comment", "ContentType": "text/plain", "CustomFields": {"Severity": "High"} }'
         'https://myrt.com/REST/2.0/ticket/6/comment'
 
+    # Comment on a ticket with custom role update
+    curl -X POST -H "Content-Type: application/json" -u 'root:password'
+        -d '{ "Content": "Testing a comment", "ContentType": "text/plain", "CustomRoles": {"Manager": "manager at example.com"} }'
+        'https://myrt.com/REST/2.0/ticket/6/comment'
+
 =head3 Transactions
 
     GET /transactions?query=<JSON>

commit d20d5605523b6ea76c3a04d4833ab7f5ba4975d0
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Fri Jan 15 14:03:31 2021 -0500

    Add unit test for updating custom roles on ticket comment/correspond

diff --git a/lib/RT/Extension/REST2/Test.pm.in b/lib/RT/Extension/REST2/Test.pm.in
index 3ff37c7..fb1cdaa 100644
--- a/lib/RT/Extension/REST2/Test.pm.in
+++ b/lib/RT/Extension/REST2/Test.pm.in
@@ -46,6 +46,7 @@ sub mech { RT::Extension::REST2::Test::Mechanize->new }
         $u->Create(
             Name => 'test',
             Password => 'password',
+            EmailAddress => 'test at rt.example',
             Privileged => 1,
         );
         return $u;
diff --git a/xt/ticket-correspond-customroles.t b/xt/ticket-correspond-customroles.t
new file mode 100644
index 0000000..7087096
--- /dev/null
+++ b/xt/ticket-correspond-customroles.t
@@ -0,0 +1,253 @@
+use strict;
+use warnings;
+use RT::Extension::REST2::Test tests => undef;
+use Test::Deep;
+
+BEGIN {
+    plan skip_all => 'RT 4.4 required'
+        unless RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+}
+
+my $mech = RT::Extension::REST2::Test->mech;
+
+my $auth = RT::Extension::REST2::Test->authorization_header;
+my $rest_base_path = '/REST/2.0';
+my $user = RT::Extension::REST2::Test->user;
+
+# Set up a couple of custom roles
+my $queue = RT::Test->load_or_create_queue( Name => "General" );
+
+my $single = RT::CustomRole->new(RT->SystemUser);
+my ($ok, $msg) = $single->Create(Name => 'Single Member', MaxValues => 1);
+ok($ok, $msg);
+my $single_id = $single->Id;
+
+($ok, $msg) = $single->AddToObject($queue->id);
+ok($ok, $msg);
+
+my $multi = RT::CustomRole->new(RT->SystemUser);
+($ok, $msg) = $multi->Create(Name => 'Multi Member');
+ok($ok, $msg);
+my $multi_id = $multi->Id;
+
+($ok, $msg) = $multi->AddToObject($queue->id);
+ok($ok, $msg);
+
+$user->PrincipalObj->GrantRight( Right => $_ )
+    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers SeeGroup SeeQueue/;
+
+# Ticket Creation
+my ($ticket_url, $ticket_id);
+{
+    my $payload = {
+        Subject => 'Ticket creation using REST',
+        Queue   => 'General',
+        Content => 'Testing ticket creation using REST API.',
+    };
+
+    my $res = $mech->post_json("$rest_base_path/ticket",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    ok($ticket_url = $res->header('location'));
+    ok(($ticket_id) = $ticket_url =~ qr[/ticket/(\d+)]);
+}
+
+# Ticket Display
+{
+    $user->PrincipalObj->GrantRight( Right => 'ShowTicket' );
+
+    my $res = $mech->get($ticket_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+
+    is($content->{id}, $ticket_id);
+    is($content->{Type}, 'ticket');
+    is($content->{Status}, 'new');
+    is($content->{Subject}, 'Ticket creation using REST');
+
+    ok(exists $content->{$_}, "Content exists for $_") for qw(AdminCc TimeEstimated Started Cc
+                                     LastUpdated TimeWorked Resolved
+                                     RT::CustomRole-1 RT::CustomRole-2
+                                     Created Due Priority EffectiveId);
+}
+
+diag "Correspond with custom roles";
+{
+    $user->PrincipalObj->GrantRight( Right => 'ReplyToTicket' );
+
+    my $res = $mech->get($ticket_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+
+    my ($hypermedia) = grep { $_->{ref} eq 'correspond' } @{ $content->{_hyperlinks} };
+    ok($hypermedia, 'got correspond hypermedia');
+    like($hypermedia->{_url}, qr[$rest_base_path/ticket/$ticket_id/correspond$]);
+
+    my $correspond_url = $mech->url_for_hypermedia('correspond');
+    my $comment_url = $correspond_url;
+    $comment_url =~ s/correspond/comment/;
+
+    $res = $mech->post_json($correspond_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Single Member' => 'foo at bar.example',
+                'Multi Member' => 'quux at cabbage.example',
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    $content = $mech->json_response;
+
+    # Because CustomRoles are set in an unpredictable order, sort the
+    # responses so we have a predictable order.
+    @$content = sort { $a cmp $b } (@$content);
+    cmp_deeply($content, ['Added quux at cabbage.example as Multi Member for this ticket', re(qr/Correspondence added|Message recorded/), 'Single Member changed from Nobody to foo at bar.example']);
+    like($res->header('Location'), qr{$rest_base_path/transaction/\d+$});
+    $res = $mech->get($res->header('Location'),
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    $content = $mech->json_response;
+    is($content->{Type}, 'Correspond');
+    is($content->{TimeTaken}, 0);
+    is($content->{Object}{type}, 'ticket');
+    is($content->{Object}{id}, $ticket_id);
+
+    $res = $mech->get($mech->url_for_hypermedia('attachment'),
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    $content = $mech->json_response;
+    is($content->{Content}, 'Hello from hypermedia!');
+    is($content->{ContentType}, 'text/plain');
+
+    # Load the ticket and check the custom roles
+    my $ticket = RT::Ticket->new($user);
+    $ticket->Load($ticket_id);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo at bar.example',
+       "Single Member role set correctly");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'quux at cabbage.example',
+       "Multi Member role set correctly");
+}
+
+diag "Comment with custom roles";
+{
+    $user->PrincipalObj->GrantRight( Right => 'CommentOnTicket' );
+
+    my $res = $mech->get($ticket_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+
+    my ($hypermedia) = grep { $_->{ref} eq 'comment' } @{ $content->{_hyperlinks} };
+    ok($hypermedia, 'got comment hypermedia');
+    like($hypermedia->{_url}, qr[$rest_base_path/ticket/$ticket_id/comment$]);
+
+    my $comment_url = $mech->url_for_hypermedia('comment');
+
+    $res = $mech->post_json($comment_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Single Member' => 'foo-new at bar.example',
+                'Multi Member' => 'quux-new at cabbage.example',
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    # Load the ticket and check the custom roles
+    my $ticket = RT::Ticket->new($user);
+    $ticket->Load($ticket_id);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo-new at bar.example',
+       "Single Member role set correctly");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'quux-new at cabbage.example',
+       "Multi Member role updated correctly");
+
+    # Supply an array for multi-member role
+    $res = $mech->post_json($comment_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Multi Member' => ['abacus at example.com', 'quux-new at cabbage.example'],
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo-new at bar.example',
+       "Single Member role unchanged");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'abacus at example.com, quux-new at cabbage.example',
+       "Multi Member role set correctly");
+
+    # Add an existing user to multi-member role
+    $res = $mech->post_json($comment_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Multi Member' => 'abacus at example.com',
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo-new at bar.example',
+       "Single Member role unchanged");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'abacus at example.com',
+       "Multi Member role unchanged");
+
+    # Supply an array for single-member role
+    $res = $mech->post_json($comment_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Single Member' => ['abacus at example.com', 'quux-new at cabbage.example'],
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    $content = $mech->json_response;
+    cmp_deeply($content, ['Comments added', 'Single Member changed from foo-new at bar.example to abacus at example.com'], "Got expected respose");
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'abacus at example.com',
+       "Single Member role changed to first member of array");
+
+    # Try using a username instead of password
+    $res = $mech->post_json($comment_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Single Member' => 'test',
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    $content = $mech->json_response;
+    cmp_deeply($content, ['Comments added', 'Single Member changed from abacus at example.com to test'], "Got expected respose");
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'test at rt.example',
+       "Single Member role changed");
+}
+
+done_testing;

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list