Bug 17170: Add API route for SearchFilters
authorNick Clemens <nick@bywatersolutions.com>
Thu, 16 Sep 2021 15:54:19 +0000 (15:54 +0000)
committerTomas Cohen Arazi <tomascohen@theke.io>
Fri, 21 Oct 2022 14:15:14 +0000 (11:15 -0300)
This adds the API routes and tests

Sponsored-by: Sponsored by: Round Rock Public Library [https://www.roundrocktexas.gov/departments/library/]
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Koha/REST/V1/SearchFilter.pm [new file with mode: 0644]
api/v1/swagger/definitions.yaml [new file with mode: 0644]
api/v1/swagger/definitions/search_filter.yaml [new file with mode: 0644]
api/v1/swagger/paths/search_filters.yaml [new file with mode: 0644]
api/v1/swagger/swagger.yaml
t/db_dependent/api/v1/search_filters.t [new file with mode: 0755]
t/lib/TestBuilder.pm

diff --git a/Koha/REST/V1/SearchFilter.pm b/Koha/REST/V1/SearchFilter.pm
new file mode 100644 (file)
index 0000000..05c72e7
--- /dev/null
@@ -0,0 +1,153 @@
+package Koha::REST::V1::SearchFilter;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+use Koha::SearchFilters;
+
+use Try::Tiny qw( catch try );
+
+=head1 Name
+
+Koha::REST::V1::SearchFilters
+
+=head1 API
+
+=head2 Methods
+
+=head3 list
+
+Controller function that handles listing Koha::SearchFilter objects
+
+=cut
+
+sub list {
+    my $c = shift->openapi->valid_input or return;
+    return try {
+        my $filters_set = Koha::SearchFilters->search({});
+        my $filters = $c->objects->search( $filters_set );
+        return $c->render(
+            status  => 200,
+            openapi => $filters
+        );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+
+}
+
+=head3 get
+
+Controller function that handles retrieving a single Koha::AdvancedEditorMacro
+
+=cut
+
+sub get {
+    my $c = shift->openapi->valid_input or return;
+    my $filter = Koha::SearchFilters->find({
+        id => $c->validation->param('search_filter_id'),
+    });
+    unless ($filter) {
+        return $c->render( status  => 404,
+                           openapi => { error => "Search filter not found" } );
+    }
+
+    return $c->render( status => 200, openapi => $filter->to_api );
+}
+
+=head3 add
+
+Controller function that handles adding a new Koha::SearchFilter object
+
+=cut
+
+sub add {
+    my $c = shift->openapi->valid_input or return;
+
+    return try {
+        my $filter = Koha::SearchFilter->new_from_api( $c->validation->param('body') );
+        $filter->store->discard_changes;
+        $c->res->headers->location( $c->req->url->to_string . '/' . $filter->id );
+        return $c->render(
+            status  => 201,
+            openapi => $filter->to_api
+        );
+    }
+    catch {
+        if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
+            return $c->render(
+                status  => 409,
+                openapi => { error => $_->error, conflict => $_->duplicate_id }
+            );
+        }
+        $c->unhandled_exception($_);
+    };
+}
+
+=head3 update
+
+Controller function that handles updating a Koha::SearchFilter object
+
+=cut
+
+sub update {
+    my $c = shift->openapi->valid_input or return;
+
+    my $filter = Koha::SearchFilters->find( $c->validation->param('search_filter_id') );
+
+    if ( not defined $filter ) {
+        return $c->render( status  => 404,
+                           openapi => { error => "Object not found" } );
+    }
+
+    return try {
+        my $params = $c->req->json;
+        $filter->set_from_api( $params );
+        $filter->store->discard_changes;
+        return $c->render( status => 200, openapi => $filter->to_api );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+}
+
+=head3 delete
+
+Controller function that handles deleting a Koha::SearchFilter object
+
+=cut
+
+sub delete {
+    my $c = shift->openapi->valid_input or return;
+
+    my $filter = Koha::SearchFilters->find( $c->validation->param('search_filter_id') );
+    if ( not defined $filter ) {
+        return $c->render( status  => 404,
+                           openapi => { error => "Object not found" } );
+    }
+
+    return try {
+        $filter->delete;
+        return $c->render( status => 204, openapi => q{} );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+}
+
+1;
diff --git a/api/v1/swagger/definitions.yaml b/api/v1/swagger/definitions.yaml
new file mode 100644 (file)
index 0000000..015aefd
--- /dev/null
@@ -0,0 +1,65 @@
+---
+account_line:
+  $ref: definitions/account_line.yaml
+advancededitormacro:
+  $ref: definitions/advancededitormacro.yaml
+allows_renewal:
+  $ref: definitions/allows_renewal.yaml
+basket:
+  $ref: definitions/basket.yaml
+cashup:
+  $ref: definitions/cashup.yaml
+checkout:
+  $ref: definitions/checkout.yaml
+checkouts:
+  $ref: definitions/checkouts.yaml
+circ-rule-kind:
+  $ref: definitions/circ-rule-kind.yaml
+city:
+  $ref: definitions/city.yaml
+error:
+  $ref: definitions/error.yaml
+fund:
+  $ref: definitions/fund.yaml
+hold:
+  $ref: definitions/hold.yaml
+holds:
+  $ref: definitions/holds.yaml
+ill_backend:
+  $ref: definitions/ill_backend.yaml
+ill_backends:
+  $ref: definitions/ill_backends.yaml
+import_batch_profile:
+  $ref: definitions/import_batch_profile.yaml
+import_batch_profiles:
+  $ref: definitions/import_batch_profiles.yaml
+invoice:
+  $ref: definitions/invoice.yaml
+item:
+  $ref: definitions/item.yaml
+library:
+  $ref: definitions/library.yaml
+order:
+  $ref: definitions/order.yaml
+patron:
+  $ref: definitions/patron.yaml
+patron_account_credit:
+  $ref: definitions/patron_account_credit.yaml
+patron_balance:
+  $ref: definitions/patron_balance.yaml
+patron_extended_attribute:
+  $ref: definitions/patron_extended_attribute.yaml
+quote:
+  $ref: definitions/quote.yaml
+return_claim:
+  $ref: definitions/return_claim.yaml
+smtp_server:
+  $ref: definitions/smtp_server.yaml
+suggestion:
+  $ref: definitions/suggestion.yaml
+search_filter:
+  $ref: definitions/search_filter.yaml
+transfer_limit:
+  $ref: definitions/transfer_limit.yaml
+vendor:
+  $ref: definitions/vendor.yaml
diff --git a/api/v1/swagger/definitions/search_filter.yaml b/api/v1/swagger/definitions/search_filter.yaml
new file mode 100644 (file)
index 0000000..a60e3f0
--- /dev/null
@@ -0,0 +1,33 @@
+---
+type: object
+properties:
+  search_filter_id:
+    type: integer
+    description: internally assigned search filter identifier
+    readOnly: true
+  name:
+    description: filter name
+    type: string
+  filter_query:
+    description: filter query part
+    type:
+    - string
+    - 'null'
+  filter_limits:
+    description: filter limits part
+    type:
+    - string
+    - 'null'
+  opac:
+    description: visible on opac
+    type:
+    - boolean
+    - 'null'
+  staff_client:
+    description: visible in staff client
+    type:
+    - boolean
+    - 'null'
+additionalProperties: false
+required:
+- name
diff --git a/api/v1/swagger/paths/search_filters.yaml b/api/v1/swagger/paths/search_filters.yaml
new file mode 100644 (file)
index 0000000..5aa61e7
--- /dev/null
@@ -0,0 +1,226 @@
+---
+"/search_filters":
+  get:
+    x-mojo-to: SearchFilter#list
+    operationId: listFilters
+    tags:
+      - search_filters
+    summary: List search filters
+    produces:
+      - application/json
+    parameters:
+      - name: name
+        in: query
+        description: Case insensitive search on filter name
+        required: false
+        type: string
+      - name: filter_query
+        in: query
+        description: Search on filter query part
+        required: false
+        type: string
+      - name: filter_limits
+        in: query
+        description: Search on filter limits
+        required: false
+        type: string
+      - name: opac
+        in: query
+        description: Display in OPAC
+        required: false
+        type: boolean
+      - name: staff_client
+        in: query
+        description: Display on staff client
+        required: false
+        type: boolean
+      - $ref: "../swagger.yaml#/parameters/match"
+      - $ref: "../swagger.yaml#/parameters/order_by"
+      - $ref: "../swagger.yaml#/parameters/page"
+      - $ref: "../swagger.yaml#/parameters/per_page"
+      - $ref: "../swagger.yaml#/parameters/q_param"
+      - $ref: "../swagger.yaml#/parameters/q_body"
+      - $ref: "../swagger.yaml#/parameters/q_header"
+      - $ref: "../swagger.yaml#/parameters/request_id_header"
+    responses:
+      '200':
+        description: A list of search filters
+        schema:
+          type: array
+          items:
+            $ref: "../swagger.yaml#/definitions/search_filter"
+      '403':
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '500':
+        description: Internal error
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '503':
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        parameters: manage_search_filters
+  post:
+    x-mojo-to: SearchFilter#add
+    operationId: addSearchFilter
+    tags:
+      - search_filters
+    summary: Add search filter
+    parameters:
+      - name: body
+        in: body
+        description: A JSON object containing informations about the new search filter
+        required: true
+        schema:
+          $ref: "../swagger.yaml#/definitions/search_filter"
+    produces:
+      - application/json
+    responses:
+      '201':
+        description: Search filter added
+        schema:
+          $ref: "../swagger.yaml#/definitions/search_filter"
+      '401':
+        description: Authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '403':
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "409":
+        description: Conflict in creating the resource
+        schema:
+          $ref: ../swagger.yaml#/definitions/error
+      '500':
+        description: Internal error
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '503':
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        parameters: manage_search_filters
+"/search_filters/{search_filter_id}":
+  get:
+    x-mojo-to: SearchFilter#get
+    operationId: getSearchFilter
+    tags:
+    - search_filters
+    summary: Get search filter
+    parameters:
+    - $ref: "../swagger.yaml#/parameters/search_filter_id_pp"
+    produces:
+    - application/json
+    responses:
+      '200':
+        description: A search filter
+        schema:
+          $ref: "../swagger.yaml#/definitions/search_filter"
+      '403':
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '404':
+        description: SearchFilter not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '500':
+        description: Internal error
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '503':
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        parameters: manage_search_filters
+  put:
+    x-mojo-to: SearchFilter#update
+    operationId: updateSearchFilter
+    tags:
+    - search_filters
+    summary: Update search filter
+    parameters:
+    - $ref: "../swagger.yaml#/parameters/search_filter_id_pp"
+    - name: body
+      in: body
+      description: A search filter object
+      required: true
+      schema:
+        $ref: "../swagger.yaml#/definitions/search_filter"
+    produces:
+    - application/json
+    responses:
+      '200':
+        description: An search_filter
+        schema:
+          $ref: "../swagger.yaml#/definitions/search_filter"
+      '401':
+        description: Authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '403':
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '404':
+        description: Search filter not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '500':
+        description: Internal error
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '503':
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        parameters: manage_search_filters
+  delete:
+    x-mojo-to: SearchFilter#delete
+    operationId: deleteSearchFilter
+    tags:
+    - macros
+    summary: Delete search filter
+    parameters:
+    - $ref: "../swagger.yaml#/parameters/search_filter_id_pp"
+    produces:
+    - application/json
+    responses:
+      '204':
+        description: Searc filter deleted
+        schema:
+          type: string
+      '401':
+        description: Authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '403':
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '404':
+        description: Search filter not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '500':
+        description: Internal error
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      '503':
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        parameters: manage_search_filters
index 5bdbab2..50a35a1 100644 (file)
@@ -68,6 +68,8 @@ definitions:
     $ref: ./definitions/renewals.yaml
   return_claim:
     $ref: ./definitions/return_claim.yaml
+  search_filter:
+    $ref: ./definitions/search_filter.yaml
   smtp_server:
     $ref: ./definitions/smtp_server.yaml
   suggestion:
@@ -97,6 +99,10 @@ paths:
     $ref: ./paths/advancededitormacros.yaml#/~1advanced_editor~1macros
   /advanced_editor/macros/shared:
     $ref: ./paths/advancededitormacros.yaml#/~1advanced_editor~1macros~1shared
+  /search_filters:
+    $ref: ./paths/search_filters.yaml#/~1search_filters
+  "/search_filters/{search_filter_id}":
+    $ref: "./paths/search_filters.yaml#/~1search_filters~1{search_filter_id}"
   "/advanced_editor/macros/shared/{advancededitormacro_id}":
     $ref: "./paths/advancededitormacros.yaml#/~1advanced_editor~1macros~1shared~1{advancededitormacro_id}"
   "/advanced_editor/macros/{advancededitormacro_id}":
@@ -427,6 +433,12 @@ parameters:
     name: x-koha-request-id
     required: false
     type: integer
+  search_filter_id_pp:
+    name: search_filter_id
+    in: path
+    description: Search filter internal identifier
+    required: true
+    type: integer
   seen_pp:
     description: Item was seen flag
     in: query
diff --git a/t/db_dependent/api/v1/search_filters.t b/t/db_dependent/api/v1/search_filters.t
new file mode 100755 (executable)
index 0000000..c0b1bb7
--- /dev/null
@@ -0,0 +1,353 @@
+#!/usr/bin/env perl
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Test::More tests => 5;
+use Test::Mojo;
+use Test::Warn;
+
+use t::lib::TestBuilder;
+use t::lib::Mocks;
+
+use Koha::SearchFilters;
+use Koha::Database;
+
+my $schema  = Koha::Database->new->schema;
+my $builder = t::lib::TestBuilder->new;
+
+t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
+
+my $t = Test::Mojo->new('Koha::REST::V1');
+
+$schema->storage->txn_begin;
+
+subtest 'list() tests' => sub {
+    plan tests => 10;
+
+    Koha::SearchFilters->search()->delete();
+
+    my $patron_1 = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 3 }
+    });
+    my $password = 'thePassword123';
+    $patron_1->set_password({ password => $password, skip_validation => 1 });
+    my $userid = $patron_1->userid;
+
+    # Create test context
+    my $search_filter_1 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
+        {
+            name => 'Test1',
+            query => 'kw:this',
+            limits => 'mc-itype,phr:BK',
+            opac => 1,
+            staff_client => 1
+        }
+    });
+    my $search_filter_2 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
+        {
+            name => 'Test2',
+            query => 'kw:that',
+            limits => 'mc-itype,phr:BK',
+            opac => 0,
+            staff_client => 1
+        }
+    });
+    my $search_filter_3 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
+        {
+            name => 'Test3',
+            query => 'kw:any',
+            limits => 'mc-itype,phr:CD',
+            opac => 0,
+            staff_client => 0
+        }
+    });
+    my $search_filter_4 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
+        {
+            name => 'Test4',
+            query => 'kw:some',
+            limits => 'mc-itype,phr:CD',
+            opac => 1,
+            staff_client => 0
+        }
+    });
+
+    # Make sure we are returned with the correct amount of macros
+    $t->get_ok( "//$userid:$password@/api/v1/search_filters" )
+      ->status_is( 200, 'SWAGGER3.2.2' )
+      ->json_has('/0/search_filter_id')
+      ->json_has('/1/search_filter_id')
+      ->json_has('/2/search_filter_id')
+      ->json_has('/3/search_filter_id');
+
+    subtest 'query parameters' => sub {
+
+        plan tests => 12;
+        $t->get_ok("//$userid:$password@/api/v1/search_filters?name=" . $search_filter_2->name)
+          ->status_is(200)
+          ->json_is( [ $search_filter_2->to_api ] );
+        $t->get_ok("//$userid:$password@/api/v1/search_filters?name=NotAName")
+          ->status_is(200)
+          ->json_is( [ ] );
+        $t->get_ok("//$userid:$password@/api/v1/search_filters?filter_query=kw:any")
+          ->status_is(200)
+          ->json_is( [ $search_filter_3->to_api ] );
+        $t->get_ok("//$userid:$password@/api/v1/search_filters?filter_limits=mc-itype,phr:BK")
+          ->status_is(200)
+          ->json_is( [ $search_filter_1->to_api, $search_filter_2->to_api ] );
+    };
+
+    # Warn on unsupported query parameter
+    $t->get_ok( "//$userid:$password@/api/v1/search_filters?filter_blah=blah" )
+      ->status_is(400)
+      ->json_is( [{ path => '/query/filter_blah', message => 'Malformed query string'}] );
+
+};
+
+subtest 'get() tests' => sub {
+
+    plan tests => 9;
+
+    my $patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 3 }
+    });
+    my $password = 'thePassword123';
+    $patron->set_password({ password => $password, skip_validation => 1 });
+    my $userid = $patron->userid;
+
+    my $search_filter_1 = $builder->build_object( { class => 'Koha::SearchFilters' } );
+    my $search_filter_2 = $builder->build_object( { class => 'Koha::SearchFilters' } );
+    my $search_filter_3 = $builder->build_object( { class => 'Koha::SearchFilters' } );
+
+    $t->get_ok( "//$userid:$password@/api/v1/search_filters/" . $search_filter_1->id )
+      ->status_is( 200, 'Filter retrieved correctly' )
+      ->json_is( $search_filter_1->to_api );
+
+    my $non_existent_code = $search_filter_1->id;
+    $search_filter_1->delete;
+
+    $t->get_ok( "//$userid:$password@/api/v1/search_filters/" . $non_existent_code )
+      ->status_is(404)
+      ->json_is( '/error' => 'Search filter not found' );
+
+    $patron->flags(4)->store;
+    $t->get_ok( "//$userid:$password/api/v1/search_filters/" . $search_filter_2->id )
+      ->status_is( 401, 'Cannot search filters without permission' )
+      ->json_is( '/error' => 'Authentication failure.' );
+
+};
+
+subtest 'add() tests' => sub {
+
+    plan tests => 17;
+
+    my $authorized_patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 0 }
+    });
+    $builder->build({
+        source => 'UserPermission',
+        value  => {
+            borrowernumber => $authorized_patron->borrowernumber,
+            module_bit     => 3,
+            code           => 'manage_search_filters',
+        },
+    });
+
+    my $password = 'thePassword123';
+    $authorized_patron->set_password({ password => $password, skip_validation => 1 });
+    my $auth_userid = $authorized_patron->userid;
+
+    my $unauthorized_patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 0 }
+    });
+    $unauthorized_patron->set_password({ password => $password, skip_validation => 1 });
+    my $unauth_userid = $unauthorized_patron->userid;
+
+    my $search_filter = $builder->build_object({ class => 'Koha::SearchFilters' });
+    my $search_filter_values = $search_filter->to_api;
+    delete $search_filter_values->{search_filter_id};
+    $search_filter->delete;
+
+    # Unauthorized attempt to write
+    $t->post_ok( "//$unauth_userid:$password@/api/v1/search_filters" => json => $search_filter_values )
+      ->status_is(403);
+
+    # Authorized attempt to write invalid data
+    my $search_filter_with_invalid_field = { %$search_filter_values };
+    $search_filter_with_invalid_field->{'coffee_filter'} = 'Chemex';
+
+    $t->post_ok( "//$auth_userid:$password@/api/v1/search_filters" => json => $search_filter_with_invalid_field )
+      ->status_is(400)
+      ->json_is(
+        "/errors" => [
+            {
+                message => "Properties not allowed: coffee_filter.",
+                path    => "/body"
+            }
+        ]
+    );
+
+    # Authorized attempt to write
+    $t->post_ok( "//$auth_userid:$password@/api/v1/search_filters" => json => $search_filter_values )
+      ->status_is( 201, 'SWAGGER3.2.1' )
+      ->json_has( '/search_filter_id', 'We generated a new id' )
+      ->json_is( '/name' => $search_filter_values->{name}, 'The name matches what we supplied' )
+      ->json_is( '/query' => $search_filter_values->{query}, 'The query matches what we supplied' )
+      ->json_is( '/limits' => $search_filter_values->{limits}, 'The limits match what we supplied' )
+      ->json_is( '/opac' => $search_filter_values->{opac}, 'The limits match what we supplied' )
+      ->json_is( '/staff_client' => $search_filter_values->{staff_client}, 'The limits match what we supplied' )
+      ->header_like( Location => qr|^\/api\/v1\/search_filters\/d*|, 'Correct location' );
+
+    # save the library_id
+    my $search_filter_id = 999;
+
+    # Authorized attempt to create with existing id
+    $search_filter_values->{search_filter_id} = $search_filter_id;
+
+    $t->post_ok( "//$auth_userid:$password@/api/v1/search_filters" => json => $search_filter_values )
+      ->status_is(400)
+      ->json_is( '/errors' => [
+            {
+                message => "Read-only.",
+                path   => "/body/search_filter_id"
+            }
+        ]
+    );
+
+};
+
+subtest 'update() tests' => sub {
+    plan tests => 15;
+
+    my $authorized_patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 0 }
+    });
+    $builder->build({
+        source => 'UserPermission',
+        value  => {
+            borrowernumber => $authorized_patron->borrowernumber,
+            module_bit     => 3,
+            code           => 'manage_search_filters',
+        },
+    });
+
+    my $password = 'thePassword123';
+    $authorized_patron->set_password({ password => $password, skip_validation => 1 });
+    my $auth_userid = $authorized_patron->userid;
+
+    my $unauthorized_patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 0 }
+    });
+    $unauthorized_patron->set_password({ password => $password, skip_validation => 1 });
+    my $unauth_userid = $unauthorized_patron->userid;
+
+    my $search_filter = $builder->build_object({ class => 'Koha::SearchFilters' });
+    my $search_filter_id = $search_filter->id;
+    my $search_filter_values = $search_filter->to_api;
+    delete $search_filter_values->{search_filter_id};
+
+    # Unauthorized attempt to update
+    $t->put_ok( "//$unauth_userid:$password@/api/v1/search_filters/$search_filter_id"
+                    => json => { name => 'New unauthorized name change' } )
+      ->status_is(403);
+
+    my $search_filter_update = {
+        name => "Filter update",
+        filter_query => "ti:The hobbit",
+        filter_limits => "mc-ccode:fantasy",
+    };
+
+    my $test = $t->put_ok( "//$auth_userid:$password@/api/v1/search_filters/$search_filter_id" => json => $search_filter_update )
+      ->status_is(200, 'Authorized user can update a macro')
+      ->json_is( '/search_filter_id' => $search_filter_id, 'We get back the id' )
+      ->json_is( '/name' => $search_filter_update->{name}, 'We get back the name' )
+      ->json_is( '/filter_query' => $search_filter_update->{filter_query}, 'We get back our query' )
+      ->json_is( '/filter_limits' => $search_filter_update->{filter_limits}, 'We get back our limits' )
+      ->json_is( '/opac' => 1, 'We get back our opac visibility unchanged' )
+      ->json_is( '/staff_client' => 1, 'We get back our staff client visibility unchanged' );
+
+    # Authorized attempt to write invalid data
+    my $search_filter_with_invalid_field = { %$search_filter_update };
+    $search_filter_with_invalid_field->{'coffee_filter'} = 'Chemex';
+
+    $t->put_ok( "//$auth_userid:$password@/api/v1/search_filters/$search_filter_id" => json => $search_filter_with_invalid_field )
+      ->status_is(400)
+      ->json_is(
+        "/errors" => [
+            {
+                message => "Properties not allowed: coffee_filter.",
+                path    => "/body"
+            }
+        ]
+    );
+
+    my $non_existent_macro = $builder->build_object({class => 'Koha::SearchFilters'});
+    my $non_existent_code = $non_existent_macro->id;
+    $non_existent_macro->delete;
+
+    $t->put_ok("//$auth_userid:$password@/api/v1/search_filters/$non_existent_code" => json => $search_filter_update)
+      ->status_is(404);
+
+};
+
+subtest 'delete() tests' => sub {
+    plan tests => 4;
+
+    my $authorized_patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 0 }
+    });
+    $builder->build({
+        source => 'UserPermission',
+        value  => {
+            borrowernumber => $authorized_patron->borrowernumber,
+            module_bit     => 3,
+            code           => 'manage_search_filters',
+        },
+    });
+
+    my $password = 'thePassword123';
+    $authorized_patron->set_password({ password => $password, skip_validation => 1 });
+    my $auth_userid = $authorized_patron->userid;
+
+    my $unauthorized_patron = $builder->build_object({
+        class => 'Koha::Patrons',
+        value => { flags => 0 }
+    });
+    $unauthorized_patron->set_password({ password => $password, skip_validation => 1 });
+    my $unauth_userid = $unauthorized_patron->userid;
+
+    my $search_filter = $builder->build_object({ class => 'Koha::SearchFilters' });
+    my $search_filter_2 = $builder->build_object({ class => 'Koha::SearchFilters' });
+    my $search_filter_id = $search_filter->id;
+    my $search_filter_2_id = $search_filter_2->id;
+
+    # Unauthorized attempt to delete
+    $t->delete_ok( "//$unauth_userid:$password@/api/v1/search_filters/$search_filter_2_id")
+      ->status_is(403, "Cannot delete search filter without permission");
+
+    $t->delete_ok( "//$auth_userid:$password@/api/v1/search_filters/$search_filter_id")
+      ->status_is( 204, 'Can delete search filter with permission');
+
+};
+
+$schema->storage->txn_rollback;
index d6e88e7..f648af7 100644 (file)
@@ -628,6 +628,10 @@ sub _gen_default_values {
             status => 'staged',
             import_error => undef
         },
+        SearchFilter => {
+            opac => 1,
+            staff_client => 1
+        },
     };
 }