[Bps-public-commit] rt-extension-rest2 branch, rest2_api_custom_field_admin, updated. 1.07-9-gf3d73eb

Aaron Trevena ast at bestpractical.com
Tue Apr 7 08:12:10 EDT 2020


The branch, rest2_api_custom_field_admin has been updated
       via  f3d73ebc2b8831ea2019214053dca36c1cf7cb78 (commit)
       via  1233ed1a6ac9a85b4e66d71f3c52498cbb350aed (commit)
       via  10ed40cf359a8f690b4fcb4c1cf937ffc61edfee (commit)
       via  89628cdaf85497a0079bb7097f7fab4c15189045 (commit)
       via  9afc1ec286491cf3f26da3e57207655b67029c1f (commit)
       via  c9aafdfb7248a6ed4137bfe4f619b06a33d51b57 (commit)
      from  5abdab89323930fd1a7924722bd7f532b09beaea (commit)

Summary of changes:
 MANIFEST                                           |   3 +
 README                                             |  25 +++
 lib/RT/Extension/REST2.pm                          |  39 ++++
 lib/RT/Extension/REST2/Resource/CustomField.pm     |  34 ++-
 .../Extension/REST2/Resource/CustomFieldValue.pm   | 106 ++++++++++
 .../Extension/REST2/Resource/CustomFieldValues.pm  |  66 ++++++
 lib/RT/Extension/REST2/Resource/CustomFields.pm    |  27 +++
 t/customfields.t                                   |  86 ++++++++
 xt/customfields.t                                  | 171 ++++++++++++++-
 xt/customfieldvalues.t                             | 232 +++++++++++++++++++++
 10 files changed, 778 insertions(+), 11 deletions(-)
 create mode 100644 lib/RT/Extension/REST2/Resource/CustomFieldValue.pm
 create mode 100644 lib/RT/Extension/REST2/Resource/CustomFieldValues.pm
 create mode 100644 t/customfields.t
 create mode 100644 xt/customfieldvalues.t

- Log -----------------------------------------------------------------
commit c9aafdfb7248a6ed4137bfe4f619b06a33d51b57
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Oct 8 13:28:33 2018 +0200

    Add all CRUD operations for CustomField
    
    Based on public github PR #12

diff --git a/README b/README
index 40138e3..a77e886 100644
--- a/README
+++ b/README
@@ -482,9 +482,18 @@ USAGE
         POST /customfields
             search for custom fields using L</JSON searches> syntax
 
+        POST /customfield
+            create a customfield; provide JSON content
+
         GET /customfield/:id
             retrieve a custom field
 
+        PUT /customfield/:id
+            update a custom field's metadata; provide JSON content
+
+        DELETE /customfield/:id
+            disable customfield
+
    Custom Roles
         GET /customroles?query=<JSON>
         POST /customroles
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 827f469..8723db8 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -528,12 +528,21 @@ Below are some examples using the endpoints above.
     POST /customfields
         search for custom fields using L</JSON searches> syntax
 
+    POST /customfield
+        create a customfield; provide JSON content
+
     GET /customfield/:id
         retrieve a custom field, with values if type is Select
 
     GET /customfield/:id?category=<category name>
         retrieve a custom field, with values filtered by category if type is Select
 
+    PUT /customfield/:id
+        update a custom field's metadata; provide JSON content
+
+    DELETE /customfield/:id
+        disable customfield
+
 =head3 Custom Roles
 
     GET /customroles?query=<JSON>
diff --git a/lib/RT/Extension/REST2/Resource/CustomField.pm b/lib/RT/Extension/REST2/Resource/CustomField.pm
index ddc9aa7..bd927c6 100644
--- a/lib/RT/Extension/REST2/Resource/CustomField.pm
+++ b/lib/RT/Extension/REST2/Resource/CustomField.pm
@@ -8,7 +8,9 @@ use namespace::autoclean;
 extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable',
         => { -alias => { serialize => '_default_serialize' } },
-     'RT::Extension::REST2::Resource::Record::Hypermedia';
+     'RT::Extension::REST2::Resource::Record::Hypermedia',
+     'RT::Extension::REST2::Resource::Record::DeletableByDisabling',
+     'RT::Extension::REST2::Resource::Record::Writable';
 
 sub dispatch_rules {
     Path::Dispatcher::Rule::Regex->new(
@@ -36,6 +38,21 @@ sub serialize {
     return $data;
 }
 
+sub forbidden {
+    my $self = shift;
+    my $method = $self->request->method;
+    if ($self->record->id) {
+        if ($method eq 'GET') {
+            return !$self->record->CurrentUserHasRight('SeeCustomField');
+        } else {
+            return !($self->record->CurrentUserHasRight('SeeCustomField') && $self->record->CurrentUserHasRight('AdminCustomField'));
+        }
+    } else {
+        return !$self->current_user->HasRight(Right => "AdminCustomField", Object => RT->System);
+    }
+    return 0;
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;

commit 9afc1ec286491cf3f26da3e57207655b67029c1f
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Oct 8 13:29:11 2018 +0200

    Add tests for all CRUD operations for CustomField
    
    Based on public github PR #12

diff --git a/xt/customfields.t b/xt/customfields.t
index 766e153..396bfb0 100644
--- a/xt/customfields.t
+++ b/xt/customfields.t
@@ -8,10 +8,6 @@ my $auth = RT::Extension::REST2::Test->authorization_header;
 my $rest_base_path = '/REST/2.0';
 my $user = RT::Extension::REST2::Test->user;
 
-my $freeform_cf = RT::CustomField->new(RT->SystemUser);
-$freeform_cf->Create(Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
-my $freeform_cf_id = $freeform_cf->id;
-
 my $select_cf = RT::CustomField->new(RT->SystemUser);
 $select_cf->Create(Name => 'Select CF', Type => 'Select', MaxValues => 1, Queue => 'General');
 $select_cf->AddValue(Name => 'First Value', SortOder => 0);
@@ -27,8 +23,58 @@ $basedon_cf->AddValue(Name => 'With No Value', SortOder => 0);
 my $basedon_cf_id = $basedon_cf->id;
 my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
 
+my $freeform_cf;
+my $freeform_cf_id;
+
+# Right test - create customfield without SeeCustomField nor AdminCustomField
+{
+    my $payload = {
+        Name      => 'Freeform CF',
+        Type      => 'Freeform',
+        MaxValues => 1,
+    };
+    my $res = $mech->post_json("$rest_base_path/customfield",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+    is($res->message, 'Forbidden');
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    my ($ok, $msg) = $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->id, undef);
+    ok(!$ok);
+    is($msg, 'Not found');
+}
+
+# Customfield create
+{
+    $user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
+    $user->PrincipalObj->GrantRight( Right => 'AdminCustomField' );
+    my $payload = {
+        Name       => 'Freeform CF',
+        Type       => 'Freeform',
+        LookupType => 'RT::Queue-RT::Ticket',
+        MaxValues  => 1,
+    };
+    my $res = $mech->post_json("$rest_base_path/customfield",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    $freeform_cf_id = $freeform_cf->id;
+    is($freeform_cf->id, 4);
+    is($freeform_cf->Description, '');
+}
+
+
 # Right test - search all tickets customfields without SeeCustomField
 {
+    $user->PrincipalObj->RevokeRight( Right => 'SeeCustomField' );
+
     my $res = $mech->post_json("$rest_base_path/customfields",
         [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
         'Authorization' => $auth,
@@ -56,7 +102,7 @@ my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
     is($content->{count}, 3);
     my $items = $content->{items};
     is(scalar(@$items), 3);
-    
+
     is($items->[0]->{type}, 'customfield');
     is($items->[0]->{id}, $freeform_cf->id);
     like($items->[0]->{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
@@ -217,5 +263,99 @@ my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
     is_deeply($values, ['With No Value']);
 }
 
+
+# Display customfield
+{
+    $user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
+
+    my $res = $mech->get("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $freeform_cf_id);
+    is($content->{Name}, 'Freeform CF');
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Freeform');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $freeform_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
+}
+
+# Right test - update customfield without AdminCustomField
+{
+    $user->PrincipalObj->RevokeRight( Right => 'AdminCustomField' );
+
+    my $payload = {
+        Description  => 'This is a CF for testing REST CRUD on CFs',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$freeform_cf_id",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+    is($res->message, 'Forbidden');
+}
+
+# Update customfield
+{
+    $user->PrincipalObj->GrantRight( Right => 'AdminCustomField' );
+
+    my $payload = {
+        Description  => 'This is a CF for testing REST CRUD on CFs',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$freeform_cf_id",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->id, $freeform_cf_id);
+    is($freeform_cf->Description, 'This is a CF for testing REST CRUD on CFs');
+}
+
+# Right test - delete customfield without AdminCustomField
+{
+    $user->PrincipalObj->RevokeRight( Right => 'AdminCustomField' );
+
+    my $res = $mech->delete("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+    is($res->message, 'Forbidden');
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->Disabled, 0);
+}
+
+# Delete customfield
+{
+    $user->PrincipalObj->GrantRight( Right => 'AdminCustomField' );
+
+    my $res = $mech->delete("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 204);
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->Disabled, 1);
+}
+
+
 done_testing;
 

commit 89628cdaf85497a0079bb7097f7fab4c15189045
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Oct 9 13:55:53 2018 +0200

    Add customfieldvalue(s) endpoints
    
    Add customfieldvalues endpoints in hypermedia links of customfield
    
    Based on public github PR #13

diff --git a/README b/README
index a77e886..44ad0b1 100644
--- a/README
+++ b/README
@@ -493,6 +493,22 @@ USAGE
 
         DELETE /customfield/:id
             disable customfield
+   Custom Field Values
+        GET /customfield/:id/values?query=<JSON>
+        POST /customfield/:id/values
+            search for values of a custom field  using L</JSON searches> syntax
+
+        POST /customfield/:id/value
+            add a value to a custom field; provide JSON content
+
+        GET /customfield/:id/value/:id
+            retrieve a value of a custom field
+
+        PUT /customfield/:id/value/:id
+            update a value of a custom field; provide JSON content
+
+        DELETE /customfield/:id/value/:id
+            remove a value from a custom field
 
    Custom Roles
         GET /customroles?query=<JSON>
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 8723db8..7b17587 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -543,6 +543,24 @@ Below are some examples using the endpoints above.
     DELETE /customfield/:id
         disable customfield
 
+=head3 Custom Field Values
+
+    GET /customfield/:id/values?query=<JSON>
+    POST /customfield/:id/values
+        search for values of a custom field  using L</JSON searches> syntax
+
+    POST /customfield/:id/value
+        add a value to a custom field; provide JSON content
+
+    GET /customfield/:id/value/:id
+        retrieve a value of a custom field
+
+    PUT /customfield/:id/value/:id
+        update a value of a custom field; provide JSON content
+
+    DELETE /customfield/:id/value/:id
+        remove a value from a custom field
+
 =head3 Custom Roles
 
     GET /customroles?query=<JSON>
diff --git a/lib/RT/Extension/REST2/Resource/CustomField.pm b/lib/RT/Extension/REST2/Resource/CustomField.pm
index bd927c6..399c5b5 100644
--- a/lib/RT/Extension/REST2/Resource/CustomField.pm
+++ b/lib/RT/Extension/REST2/Resource/CustomField.pm
@@ -8,7 +8,8 @@ use namespace::autoclean;
 extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable',
         => { -alias => { serialize => '_default_serialize' } },
-     'RT::Extension::REST2::Resource::Record::Hypermedia',
+     'RT::Extension::REST2::Resource::Record::Hypermedia'
+        => { -alias => { hypermedia_links => '_default_hypermedia_links' } },
      'RT::Extension::REST2::Resource::Record::DeletableByDisabling',
      'RT::Extension::REST2::Resource::Record::Writable';
 
@@ -53,8 +54,20 @@ sub forbidden {
     return 0;
 }
 
+sub hypermedia_links {
+    my $self = shift;
+    my $links = $self->_default_hypermedia_links(@_);
+
+    if ($self->record->IsSelectionType) {
+        push @$links, {
+            ref  => 'customfieldvalues',
+            _url => RT::Extension::REST2->base_uri . "/customfield/" . $self->record->id . "/values",
+        };
+    }
+    return $links;
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;
 
-
diff --git a/lib/RT/Extension/REST2/Resource/CustomFieldValue.pm b/lib/RT/Extension/REST2/Resource/CustomFieldValue.pm
new file mode 100644
index 0000000..5be6acb
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/CustomFieldValue.pm
@@ -0,0 +1,106 @@
+package RT::Extension::REST2::Resource::CustomFieldValue;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+use RT::Extension::REST2::Util qw(expand_uid);
+
+extends 'RT::Extension::REST2::Resource::Record';
+with 'RT::Extension::REST2::Resource::Record::Readable',
+     'RT::Extension::REST2::Resource::Record::Hypermedia',
+     'RT::Extension::REST2::Resource::Record::Deletable',
+     'RT::Extension::REST2::Resource::Record::Writable';
+
+has 'customfield' => (
+    is  => 'ro',
+    isa => 'RT::CustomField',
+);
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/customfield/(\d+)/value/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $cf_id = $match->pos(1);
+            my $cf = RT::CustomField->new($req->env->{"rt.current_user"});
+            $cf->Load($cf_id);
+            return { record_class => 'RT::CustomFieldValue', customfield => $cf }
+        },
+    ),
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/customfield/(\d+)/value/(\d+)/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $cf_id = $match->pos(1);
+            my $cf = RT::CustomField->new($req->env->{"rt.current_user"});
+            $cf->Load($cf_id);
+            return { record_class => 'RT::CustomFieldValue', record_id => shift->pos(2), customfield => $cf }
+        },
+    )
+}
+
+sub forbidden {
+    my $self = shift;
+    my $method = $self->request->method;
+    if ($method eq 'GET') {
+        return !$self->customfield->CurrentUserHasRight('SeeCustomField');
+    } else {
+        return !($self->customfield->CurrentUserHasRight('AdminCustomField') ||$self->customfield->CurrentUserHasRight('AdminCustomFieldValues'));
+    }
+}
+
+sub create_record {
+    my $self = shift;
+    my $data = shift;
+    
+    my ($ok, $msg) = $self->customfield->AddValue(%$data);
+    $self->record->Load($ok) if $ok;
+    return ($ok, $msg);
+}
+
+sub delete_resource {
+    my $self = shift;
+
+    my ($ok, $msg) = $self->customfield->DeleteValue($self->record->id);
+    return $ok;
+}
+
+sub hypermedia_links {
+    my $self = shift;
+    my $record = $self->record;
+    my $cf = $self->customfield;
+
+    my $class = blessed($record);
+    $class =~ s/^RT:://;
+    $class = lc $class;
+    my $id = $record->id;
+
+    my $cf_class = blessed($cf);
+    $cf_class =~ s/^RT:://;
+    $cf_class = lc $cf_class;
+    my $cf_id = $cf->id;
+
+    my $cf_entry = expand_uid($cf->UID);
+
+    my $links = [
+        {
+            ref  => 'self',
+            type => $class,
+            id   => $id,
+            _url => RT::Extension::REST2->base_uri . "/$cf_class/$cf_id/$class/$id",
+        },
+        {
+            %$cf_entry,
+            ref  => 'customfield',
+        },
+    ];
+
+    return $links;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+
diff --git a/lib/RT/Extension/REST2/Resource/CustomFieldValues.pm b/lib/RT/Extension/REST2/Resource/CustomFieldValues.pm
new file mode 100644
index 0000000..7fd94c8
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/CustomFieldValues.pm
@@ -0,0 +1,66 @@
+package RT::Extension::REST2::Resource::CustomFieldValues;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+
+extends 'RT::Extension::REST2::Resource::Collection';
+with 'RT::Extension::REST2::Resource::Collection::QueryByJSON';
+
+has 'customfield' => (
+    is  => 'ro',
+    isa => 'RT::CustomField',
+);
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/customfield/(\d+)/values/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $cf_id = $match->pos(1);
+            my $cf = RT::CustomField->new($req->env->{"rt.current_user"});
+            $cf->Load($cf_id);
+            my $values = $cf->Values;
+            return { customfield => $cf, collection => $values }
+        },
+    )
+}
+
+sub forbidden {
+    my $self = shift;
+    my $method = $self->request->method;
+    if ($method eq 'GET') {
+        return !$self->customfield->CurrentUserHasRight('SeeCustomField');
+    } else {
+        return !($self->customfield->CurrentUserHasRight('AdminCustomField') ||$self->customfield->CurrentUserHasRight('AdminCustomFieldValues'));
+    }
+}
+
+sub serialize {
+    my $self = shift;
+    my $collection = $self->collection;
+    my $cf = $self->customfield;
+    my @results;
+
+    while (my $item = $collection->Next) {
+        my $result = {
+            type => 'customfieldvalue',
+            id   => $item->id,
+            name   => $item->Name,
+            _url => RT::Extension::REST2->base_uri . "/customfield/" . $cf->id . '/value/' . $item->id,
+        };
+        push @results, $result;
+    }
+    return {
+        count       => scalar(@results)         + 0,
+        total       => $collection->CountAll    + 0,
+        per_page    => $collection->RowsPerPage + 0,
+        page        => ($collection->FirstRow / $collection->RowsPerPage) + 1,
+        items       => \@results,
+    };
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;

commit 10ed40cf359a8f690b4fcb4c1cf937ffc61edfee
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Oct 9 13:56:57 2018 +0200

    Add tests for customfieldvalue(s) REST2 API endpoints
    
    Based on public github PR #13

diff --git a/MANIFEST b/MANIFEST
index 8ba75b7..bbfc28e 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -33,6 +33,8 @@ lib/RT/Extension/REST2/Resource/Collection/ProcessPOSTasGET.pm
 lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
 lib/RT/Extension/REST2/Resource/CustomField.pm
 lib/RT/Extension/REST2/Resource/CustomFields.pm
+lib/RT/Extension/REST2/Resource/CustomFieldValue.pm
+lib/RT/Extension/REST2/Resource/CustomFieldValues.pm
 lib/RT/Extension/REST2/Resource/CustomRole.pm
 lib/RT/Extension/REST2/Resource/CustomRoles.pm
 lib/RT/Extension/REST2/Resource/Group.pm
@@ -71,6 +73,7 @@ xt/assets.t
 xt/catalogs.t
 xt/conflict.t
 xt/customfields.t
+xt/customfieldvalues.t
 xt/group-members.t
 xt/not_found.t
 xt/organization.t
diff --git a/xt/customfields.t b/xt/customfields.t
index 396bfb0..fec838d 100644
--- a/xt/customfields.t
+++ b/xt/customfields.t
@@ -163,12 +163,15 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $select_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
 
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$select_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['First Value', 'Second Value', 'Third Value']);
 }
@@ -193,12 +196,15 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $basedon_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
 
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['With First Value', 'With No Value']);
 }
@@ -223,12 +229,15 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $basedon_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
 
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['With First Value']);
 }
@@ -253,12 +262,16 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $basedon_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
 
+
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['With No Value']);
 }
diff --git a/xt/customfieldvalues.t b/xt/customfieldvalues.t
new file mode 100644
index 0000000..f691261
--- /dev/null
+++ b/xt/customfieldvalues.t
@@ -0,0 +1,232 @@
+use strict;
+use warnings;
+use lib 't/lib';
+use RT::Extension::REST2::Test tests => undef;
+
+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;
+
+my $select_cf = RT::CustomField->new(RT->SystemUser);
+$select_cf->Create(Name => 'Select CF', Type => 'Select', MaxValues => 1);
+$select_cf->AddValue(Name => 'First Value', SortOrder => 0);
+$select_cf->AddValue(Name => 'Second Value', SortOrder => 1);
+$select_cf->AddValue(Name => 'Third Value', SortOrder => 2);
+my $select_cf_id = $select_cf->id;
+my $select_cf_values = $select_cf->Values->ItemsArrayRef;
+
+my $basedon_cf = RT::CustomField->new(RT->SystemUser);
+$basedon_cf->Create(Name => 'SubSelect CF', Type => 'Select', MaxValues => 1, BasedOn => $select_cf->id);
+$basedon_cf->AddValue(Name => 'With First Value', Category => $select_cf_values->[0]->Name, SortOder => 0);
+$basedon_cf->AddValue(Name => 'With No Value', SortOder => 0);
+my $basedon_cf_id = $basedon_cf->id;
+my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
+
+# Right test - retrieve all values without SeeCustomField
+{
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id/values",
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+}
+
+$user->PrincipalObj->GrantRight(Right => 'SeeCustomField');
+
+# Retrieve customfield's hypermedia link for customfieldvalues
+{
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    my $links = $content->{_hyperlinks};
+    my @cfvs_links = grep { $_->{ref} eq 'customfieldvalues' } @$links;
+    is(scalar(@cfvs_links), 1);
+    like($cfvs_links[0]->{_url}, qr{$rest_base_path/customfield/$select_cf_id/values$});
+}
+
+# No customfieldvalues hypermedia link for non-select customfield
+{
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Create(Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
+    my $freeform_cf_id = $freeform_cf->id;
+
+    my $res = $mech->get("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    my $links = $content->{_hyperlinks};
+    my @cfvs_links = grep { $_->{ref} eq 'customfieldvalues' } @$links;
+    is(scalar(@cfvs_links), 0);
+}
+
+# Retrieve all values
+{
+
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id/values",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 3);
+    is($content->{count}, 3);
+    my $items = $content->{items};
+    is(scalar(@$items), 3);
+
+    for (my $i=0; $i < scalar @$items; $i++) {
+        my $cf_value_id = $select_cf_values->[$i]->id;
+        is($items->[$i]->{type}, 'customfieldvalue');
+        is($items->[$i]->{id}, $cf_value_id);
+        is($items->[$i]->{name}, $select_cf_values->[$i]->Name);
+        like($items->[$i]->{_url}, qr{$rest_base_path/customfield/$select_cf_id/value/$cf_value_id$});
+    }
+}
+
+# Right test - udpate a value without AdminCustomFieldValues nor AdminCustomField
+{
+    my $payload = {
+        Name => 'Third and Last Value',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is($select_cf_values->[-1]->Name, 'Third Value');
+}
+
+# Right test - udpate a value without AdminCustomFieldValues but with AdminCustomField
+{
+    $user->PrincipalObj->GrantRight(Right => 'AdminCustomField');
+
+    my $payload = {
+        Name => 'Third and Last Value',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is($select_cf_values->[-1]->Name, 'Third and Last Value');
+}
+
+# Right test - udpate a value without AdminCustomField but with AdminCustomFieldValues
+{
+    $user->PrincipalObj->RevokeRight(Right => 'AdminCustomField');
+    $user->PrincipalObj->GrantRight(Right => 'AdminCustomFieldValues', Object => $select_cf);
+    $user->PrincipalObj->GrantRight(Right => 'AdminCustomFieldValues', Object => $basedon_cf);
+
+    my $payload = {
+        Name => 'Third and Last but NOT Least Value',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is($select_cf_values->[-1]->Name, 'Third and Last but NOT Least Value');
+}
+
+# Add a value
+{
+    my $payload = {
+        Name      => 'Fourth Value',
+        SortOrder => 3,
+    };
+    my $res = $mech->post_json("$rest_base_path/customfield/$select_cf_id/value",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is(scalar(@$select_cf_values), 4);
+    is($select_cf_values->[-1]->Name, 'Fourth Value');
+}
+
+# Retrieve a value
+{
+    my $cfv = $select_cf_values->[-2];
+    my $cfv_id = $cfv->id;
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id/value/$cfv_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    foreach my $field (qw/id Name Description SortOrder Category/) {
+        is($content->{$field}, $cfv->$field);
+    }
+    
+    ok(exists $content->{$_}, "got $_") for qw/Created Creator LastUpdated LastUpdatedBy/;
+    
+    is($content->{CustomField}->{id}, $select_cf_id);
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 2);
+
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $cfv_id);
+    is($links->[0]{type}, 'customfieldvalue');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$select_cf_id/customfieldvalue/$cfv_id$});
+
+    is($links->[1]{ref}, 'customfield');
+    is($links->[1]{id}, $select_cf_id);
+    is($links->[1]{type}, 'customfield');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
+}
+
+# Retrieve all values filtered by category
+{
+    my $payload = [
+        {
+            field => 'Category',
+            value => $select_cf_values->[0]->Name,
+        }
+    ];
+    my $res = $mech->post_json("$rest_base_path/customfield/$basedon_cf_id/values",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 1);
+    is($content->{count}, 1);
+    my $items = $content->{items};
+    is(scalar(@$items), 1);
+
+    for (my $i=0; $i < scalar @$items; $i++) {
+        my $cf_value_id = $basedon_cf_values->[$i]->id;
+        is($items->[$i]->{type}, 'customfieldvalue');
+        is($items->[$i]->{id}, $cf_value_id);
+        is($items->[$i]->{name}, $basedon_cf_values->[$i]->Name);
+        like($items->[$i]->{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/value/$cf_value_id$});
+    }
+}
+
+# Delete a value
+{
+    my $res = $mech->delete("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        'Authorization' => $auth,
+    );
+    is($res->code, 204);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is(scalar(@$select_cf_values), 3);
+    is($select_cf_values->[-1]->Name, 'Third and Last but NOT Least Value');
+}
+
+done_testing;
+

commit 1233ed1a6ac9a85b4e66d71f3c52498cbb350aed
Author: gibus <gibus at easter-eggs.com>
Date:   Sun Oct 21 10:28:08 2018 +0200

    Add searching for customfields attached to a catalog/class/queue
    
    Add documentation for searching for customfields attached to a catalog/class/queue
    
    Based on public github PR #22

diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 7b17587..4cc1514 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -531,6 +531,18 @@ Below are some examples using the endpoints above.
     POST /customfield
         create a customfield; provide JSON content
 
+    GET /catalog/:id/customfields?query=<JSON>
+    POST /catalog/:id/customfields
+        search for custom fields attached to a catalog using L</JSON searches> syntax
+
+    GET /class/:id/customfields?query=<JSON>
+    POST /class/:id/customfields
+        search for custom fields attached to a class using L</JSON searches> syntax
+
+    GET /queue/:id/customfields?query=<JSON>
+    POST /queue/:id/customfields
+        search for custom fields attached to a queue using L</JSON searches> syntax
+
     GET /customfield/:id
         retrieve a custom field, with values if type is Select
 
diff --git a/lib/RT/Extension/REST2/Resource/CustomFields.pm b/lib/RT/Extension/REST2/Resource/CustomFields.pm
index e227dca..a041010 100644
--- a/lib/RT/Extension/REST2/Resource/CustomFields.pm
+++ b/lib/RT/Extension/REST2/Resource/CustomFields.pm
@@ -8,13 +8,40 @@ use namespace::autoclean;
 extends 'RT::Extension::REST2::Resource::Collection';
 with 'RT::Extension::REST2::Resource::Collection::QueryByJSON';
 
+has 'object_applied_to' => (
+    is  => 'ro',
+    required => 0,
+);
+
 sub dispatch_rules {
     Path::Dispatcher::Rule::Regex->new(
         regex => qr{^/customfields/?$},
         block => sub { { collection_class => 'RT::CustomFields' } },
     ),
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/(catalog|class|queue)/(\d+)/customfields/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $object_type = 'RT::'. ucfirst($match->pos(1));
+            my $object_id = $match->pos(2);
+            my $object_applied_to = $object_type->new($req->env->{"rt.current_user"});
+            $object_applied_to->Load($object_id);
+            return {object_applied_to => $object_applied_to, collection_class => 'RT::CustomFields'};
+        },
+    ),
 }
 
+after 'limit_collection' => sub {
+    my $self = shift;
+    my $collection = $self->collection;
+    my $object = $self->object_applied_to;
+    if ($object && $object->id) {
+        $collection->Limit(ENTRYAGGREGATOR => "AND", FIELD => 'LookupType', OPERATOR => 'STARTSWITH', VALUE => ref($object));
+        $collection->LimitToGlobalOrObjectId($object->id);
+    }
+    return 1;
+};
+
 __PACKAGE__->meta->make_immutable;
 
 1;
diff --git a/t/customfields.t b/t/customfields.t
new file mode 100644
index 0000000..766e153
--- /dev/null
+++ b/t/customfields.t
@@ -0,0 +1,221 @@
+use strict;
+use warnings;
+use lib 't/lib';
+use RT::Extension::REST2::Test tests => undef;
+
+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;
+
+my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+$freeform_cf->Create(Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
+my $freeform_cf_id = $freeform_cf->id;
+
+my $select_cf = RT::CustomField->new(RT->SystemUser);
+$select_cf->Create(Name => 'Select CF', Type => 'Select', MaxValues => 1, Queue => 'General');
+$select_cf->AddValue(Name => 'First Value', SortOder => 0);
+$select_cf->AddValue(Name => 'Second Value', SortOrder => 1);
+$select_cf->AddValue(Name => 'Third Value', SortOrder => 2);
+my $select_cf_id = $select_cf->id;
+my $select_cf_values = $select_cf->Values->ItemsArrayRef;
+
+my $basedon_cf = RT::CustomField->new(RT->SystemUser);
+$basedon_cf->Create(Name => 'SubSelect CF', Type => 'Select', MaxValues => 1, Queue => 'General', BasedOn => $select_cf->id);
+$basedon_cf->AddValue(Name => 'With First Value', Category => $select_cf_values->[0]->Name, SortOder => 0);
+$basedon_cf->AddValue(Name => 'With No Value', SortOder => 0);
+my $basedon_cf_id = $basedon_cf->id;
+my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
+
+# Right test - search all tickets customfields without SeeCustomField
+{
+    my $res = $mech->post_json("$rest_base_path/customfields",
+        [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 3);
+    is($content->{count}, 0);
+    is_deeply($content->{items}, []);
+}
+
+# search all tickets customfields
+{
+    $user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
+
+    my $res = $mech->post_json("$rest_base_path/customfields",
+        [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 3);
+    is($content->{count}, 3);
+    my $items = $content->{items};
+    is(scalar(@$items), 3);
+    
+    is($items->[0]->{type}, 'customfield');
+    is($items->[0]->{id}, $freeform_cf->id);
+    like($items->[0]->{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
+
+    is($items->[1]->{type}, 'customfield');
+    is($items->[1]->{id}, $select_cf->id);
+    like($items->[1]->{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
+
+    is($items->[2]->{type}, 'customfield');
+    is($items->[2]->{id}, $basedon_cf->id);
+    like($items->[2]->{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+}
+
+# Freeform CustomField display
+{
+    my $res = $mech->get("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $freeform_cf_id);
+    is($content->{Name}, $freeform_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Freeform');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $freeform_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
+}
+
+# Select CustomField display
+{
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $select_cf_id);
+    is($content->{Name}, $select_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $select_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['First Value', 'Second Value', 'Third Value']);
+}
+
+# BasedOn CustomField display
+{
+    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $basedon_cf_id);
+    is($content->{Name}, $basedon_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $basedon_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['With First Value', 'With No Value']);
+}
+
+# BasedOn CustomField display with category filter
+{
+    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id?category=First%20Value",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $basedon_cf_id);
+    is($content->{Name}, $basedon_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $basedon_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['With First Value']);
+}
+
+# BasedOn CustomField display with null category filter
+{
+    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id?category=",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $basedon_cf_id);
+    is($content->{Name}, $basedon_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $basedon_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['With No Value']);
+}
+
+done_testing;
+

commit f3d73ebc2b8831ea2019214053dca36c1cf7cb78
Author: gibus <gibus at easter-eggs.com>
Date:   Sun Oct 21 10:32:07 2018 +0200

    Add tests for searching for customfields attached to a catalog/class/queue
    
    Based on public github PR #22

diff --git a/t/customfields.t b/t/customfields.t
index 766e153..6590ad2 100644
--- a/t/customfields.t
+++ b/t/customfields.t
@@ -7,44 +7,31 @@ 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;
+$user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
 
-my $freeform_cf = RT::CustomField->new(RT->SystemUser);
-$freeform_cf->Create(Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
-my $freeform_cf_id = $freeform_cf->id;
+my $queue = RT::Queue->new(RT->SystemUser);
+$queue->Load('General');
+my $queue_id = $queue->id;
 
-my $select_cf = RT::CustomField->new(RT->SystemUser);
-$select_cf->Create(Name => 'Select CF', Type => 'Select', MaxValues => 1, Queue => 'General');
-$select_cf->AddValue(Name => 'First Value', SortOder => 0);
-$select_cf->AddValue(Name => 'Second Value', SortOrder => 1);
-$select_cf->AddValue(Name => 'Third Value', SortOrder => 2);
-my $select_cf_id = $select_cf->id;
-my $select_cf_values = $select_cf->Values->ItemsArrayRef;
+my $attached_single_cf = RT::CustomField->new(RT->SystemUser);
+$attached_single_cf->Create(LookupType => 'RT::Queue-RT::Ticket', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
+my $attached_single_cf_id = $attached_single_cf->id;
 
-my $basedon_cf = RT::CustomField->new(RT->SystemUser);
-$basedon_cf->Create(Name => 'SubSelect CF', Type => 'Select', MaxValues => 1, Queue => 'General', BasedOn => $select_cf->id);
-$basedon_cf->AddValue(Name => 'With First Value', Category => $select_cf_values->[0]->Name, SortOder => 0);
-$basedon_cf->AddValue(Name => 'With No Value', SortOder => 0);
-my $basedon_cf_id = $basedon_cf->id;
-my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
+my $attached_multiple_cf = RT::CustomField->new(RT->SystemUser);
+$attached_multiple_cf->Create(LookupType => 'RT::Queue-RT::Ticket', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 0, Queue => 'General');
+my $attached_multiple_cf_id = $attached_multiple_cf->id;
 
-# Right test - search all tickets customfields without SeeCustomField
-{
-    my $res = $mech->post_json("$rest_base_path/customfields",
-        [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
-        'Authorization' => $auth,
-    );
-    is($res->code, 200);
+my $detached_cf = RT::CustomField->new(RT->SystemUser);
+$detached_cf->Create(LookupType => 'RT::Queue-RT::Ticket', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1);
+my $detached_cf_id = $detached_cf->id;
 
-    my $content = $mech->json_response;
-    is($content->{total}, 3);
-    is($content->{count}, 0);
-    is_deeply($content->{items}, []);
-}
+my $queue_cf = RT::CustomField->new(RT->SystemUser);
+$queue_cf->Create(LookupType => 'RT::Queue', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1);
+$queue_cf->AddToObject($queue);
+my $queue_cf_id = $queue_cf->id;
 
-# search all tickets customfields
+# All tickets customfields
 {
-    $user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
-
     my $res = $mech->post_json("$rest_base_path/customfields",
         [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
         'Authorization' => $auth,
@@ -54,167 +41,45 @@ my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
     my $content = $mech->json_response;
     is($content->{total}, 3);
     is($content->{count}, 3);
-    my $items = $content->{items};
-    is(scalar(@$items), 3);
-    
-    is($items->[0]->{type}, 'customfield');
-    is($items->[0]->{id}, $freeform_cf->id);
-    like($items->[0]->{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
-
-    is($items->[1]->{type}, 'customfield');
-    is($items->[1]->{id}, $select_cf->id);
-    like($items->[1]->{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
-
-    is($items->[2]->{type}, 'customfield');
-    is($items->[2]->{id}, $basedon_cf->id);
-    like($items->[2]->{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+    is(scalar @{$content->{items}}, 3);
+    my @ids = sort map {$_->{id}} @{$content->{items}};
+    is_deeply(\@ids, [$attached_single_cf_id, $attached_multiple_cf_id, $detached_cf_id]);
 }
 
-# Freeform CustomField display
+# All tickets single customfields attached to queue 'General'
 {
-    my $res = $mech->get("$rest_base_path/customfield/$freeform_cf_id",
+    my $res = $mech->post_json("$rest_base_path/queue/$queue_id/customfields",
+        [
+            {field => 'LookupType', value => 'RT::Queue-RT::Ticket'},
+            {field => 'MaxValues', value => 1},
+        ],
         'Authorization' => $auth,
     );
     is($res->code, 200);
-    my $content = $mech->json_response;
-    is($content->{id}, $freeform_cf_id);
-    is($content->{Name}, $freeform_cf->Name);
-    is($content->{Description}, '');
-    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
-    is($content->{Type}, 'Freeform');
-    is($content->{MaxValues}, 1);
-    is($content->{Disabled}, 0);
-
-    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
-    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
-    ok(exists $content->{$_}, "got $_") for @fields;
-
-    my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
-    is($links->[0]{ref}, 'self');
-    is($links->[0]{id}, $freeform_cf_id);
-    is($links->[0]{type}, 'customfield');
-    like($links->[0]{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
-}
 
-# Select CustomField display
-{
-    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id",
-        'Authorization' => $auth,
-    );
-    is($res->code, 200);
     my $content = $mech->json_response;
-    is($content->{id}, $select_cf_id);
-    is($content->{Name}, $select_cf->Name);
-    is($content->{Description}, '');
-    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
-    is($content->{Type}, 'Select');
-    is($content->{MaxValues}, 1);
-    is($content->{Disabled}, 0);
-
-    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
-    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
-    ok(exists $content->{$_}, "got $_") for @fields;
-
-    my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
-    is($links->[0]{ref}, 'self');
-    is($links->[0]{id}, $select_cf_id);
-    is($links->[0]{type}, 'customfield');
-    like($links->[0]{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
-
-    my $values = $content->{Values};
-    is_deeply($values, ['First Value', 'Second Value', 'Third Value']);
+    is($content->{total}, 1);
+    is($content->{count}, 1);
+    is(scalar @{$content->{items}}, 1);
+    is($content->{items}->[0]->{id}, $attached_single_cf_id);
 }
 
-# BasedOn CustomField display
+# All single customfields attached to queue 'General'
 {
-    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id",
+    my $res = $mech->post_json("$rest_base_path/queue/$queue_id/customfields",
+        [
+            {field => 'MaxValues', value => 1},
+        ],
         'Authorization' => $auth,
     );
     is($res->code, 200);
-    my $content = $mech->json_response;
-    is($content->{id}, $basedon_cf_id);
-    is($content->{Name}, $basedon_cf->Name);
-    is($content->{Description}, '');
-    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
-    is($content->{Type}, 'Select');
-    is($content->{MaxValues}, 1);
-    is($content->{Disabled}, 0);
-
-    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
-    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
-    ok(exists $content->{$_}, "got $_") for @fields;
-
-    my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
-    is($links->[0]{ref}, 'self');
-    is($links->[0]{id}, $basedon_cf_id);
-    is($links->[0]{type}, 'customfield');
-    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
-
-    my $values = $content->{Values};
-    is_deeply($values, ['With First Value', 'With No Value']);
-}
-
-# BasedOn CustomField display with category filter
-{
-    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id?category=First%20Value",
-        'Authorization' => $auth,
-    );
-    is($res->code, 200);
-    my $content = $mech->json_response;
-    is($content->{id}, $basedon_cf_id);
-    is($content->{Name}, $basedon_cf->Name);
-    is($content->{Description}, '');
-    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
-    is($content->{Type}, 'Select');
-    is($content->{MaxValues}, 1);
-    is($content->{Disabled}, 0);
 
-    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
-    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
-    ok(exists $content->{$_}, "got $_") for @fields;
-
-    my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
-    is($links->[0]{ref}, 'self');
-    is($links->[0]{id}, $basedon_cf_id);
-    is($links->[0]{type}, 'customfield');
-    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
-
-    my $values = $content->{Values};
-    is_deeply($values, ['With First Value']);
-}
-
-# BasedOn CustomField display with null category filter
-{
-    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id?category=",
-        'Authorization' => $auth,
-    );
-    is($res->code, 200);
     my $content = $mech->json_response;
-    is($content->{id}, $basedon_cf_id);
-    is($content->{Name}, $basedon_cf->Name);
-    is($content->{Description}, '');
-    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
-    is($content->{Type}, 'Select');
-    is($content->{MaxValues}, 1);
-    is($content->{Disabled}, 0);
-
-    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
-    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
-    ok(exists $content->{$_}, "got $_") for @fields;
-
-    my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
-    is($links->[0]{ref}, 'self');
-    is($links->[0]{id}, $basedon_cf_id);
-    is($links->[0]{type}, 'customfield');
-    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
-
-    my $values = $content->{Values};
-    is_deeply($values, ['With No Value']);
+    is($content->{total}, 2);
+    is($content->{count}, 2);
+    is(scalar @{$content->{items}}, 2);
+    my @ids = sort map {$_->{id}} @{$content->{items}};
+    is_deeply(\@ids, [$attached_single_cf_id, $queue_cf_id]);
 }
 
 done_testing;

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


More information about the Bps-public-commit mailing list