3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20 use Koha::Acquisition::Orders;
25 use Mojo::JSON qw(encode_json);
27 # Dummy app for testing the plugin
28 use Mojolicious::Lite;
30 app->log->level('error');
32 plugin 'Koha::REST::Plugin::Objects';
33 plugin 'Koha::REST::Plugin::Query';
34 plugin 'Koha::REST::Plugin::Pagination';
36 get '/cities' => sub {
38 $c->validation->output($c->req->params->to_hash);
39 my $cities = $c->objects->search(Koha::Cities->new);
40 $c->render( status => 200, json => $cities );
43 get '/cities/:city_id' => sub {
45 my $id = $c->stash("city_id");
46 my $city = $c->objects->find(Koha::Cities->new, $id);
47 $c->render( status => 200, json => $city );
50 get '/orders' => sub {
52 $c->stash('koha.embed', ( { fund => {} } ) );
53 $c->validation->output($c->req->params->to_hash);
54 my $orders = $c->objects->search(Koha::Acquisition::Orders->new);
55 $c->render( status => 200, json => $orders );
58 get '/orders/:order_id' => sub {
60 $c->stash('koha.embed', ( { fund => {} } ) );
61 my $id = $c->stash("order_id");
62 my $order = $c->objects->find(Koha::Acquisition::Orders->new, $id);
63 $c->render( status => 200, json => $order );
66 get '/biblios' => sub {
68 my $output = $c->req->params->to_hash;
69 $output->{query} = $c->req->json if defined $c->req->json;
70 my $headers = $c->req->headers->to_hash;
71 $output->{'x-koha-query'} = $headers->{'x-koha-query'} if defined $headers->{'x-koha-query'};
72 $c->validation->output($output);
73 my $biblios_set = Koha::Biblios->new;
74 $c->stash("koha.embed", {
81 my $biblios = $c->objects->search($biblios_set);
82 $c->render( status => 200, json => {count => scalar(@$biblios), biblios => $biblios} );
85 get '/libraries/:library_id_1/:library_id_2' => sub {
89 # Emulate a public route by stashing the is_public value
90 $c->stash( 'is_public' => 1 );
92 my $library_id_1 = $c->param('library_id_1');
93 my $library_id_2 = $c->param('library_id_2');
95 my $libraries_rs = Koha::Libraries->search(
96 { branchcode => [ $library_id_1, $library_id_2 ] },
97 { order_by => 'branchname' }
99 my $libraries = $c->objects->search( $libraries_rs );
107 get '/my_patrons' => sub {
111 my $patrons = $c->objects->search( scalar Koha::Patrons->search( {}, { order_by => 'borrowernumber' }) );
120 use Test::More tests => 14;
124 use t::lib::TestBuilder;
127 my $schema = Koha::Database->new()->schema();
128 my $builder = t::lib::TestBuilder->new;
130 subtest 'objects.search helper' => sub {
134 $schema->storage->txn_begin;
136 # Remove existing cities to have more control on the search results
137 Koha::Cities->delete;
139 # Create three sample cities that match the query. This makes sure we
140 # always have a "next" link regardless of Mojolicious::Plugin::OpenAPI version.
141 $builder->build_object({
142 class => 'Koha::Cities',
144 city_name => 'Manuel'
147 $builder->build_object({
148 class => 'Koha::Cities',
150 city_name => 'Manuela'
153 $builder->build_object({
154 class => 'Koha::Cities',
156 city_name => 'Manuelab'
160 my $t = Test::Mojo->new;
161 $t->get_ok('/cities?name=manuel&_per_page=1&_page=1')
163 ->header_like( 'Link' => qr/<http:\/\/.*[\?&]_page=2.*>; rel="next",/ )
166 ->json_is('/0/name' => 'Manuel');
168 $builder->build_object({
169 class => 'Koha::Cities',
171 city_name => 'Emanuel'
176 $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=starts_with')
182 ->json_is('/0/name' => 'Manuel')
183 ->json_is('/1/name' => 'Manuela')
184 ->json_is('/2/name' => 'Manuelab');
187 $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=ends_with')
192 ->json_is('/0/name' => 'Manuel')
193 ->json_is('/1/name' => 'Emanuel');
196 $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=exact')
200 ->json_is('/0/name' => 'Manuel');
203 $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=contains')
210 ->json_is('/0/name' => 'Manuel')
211 ->json_is('/1/name' => 'Manuela')
212 ->json_is('/2/name' => 'Manuelab')
213 ->json_is('/3/name' => 'Emanuel');
217 $builder->build_object({ class => 'Koha::Cities' });
220 t::lib::Mocks::mock_preference('RESTdefaultPageSize', 20 );
221 $t->get_ok('/cities')
224 my $response_count = scalar @{ $t->tx->res->json };
225 is( $response_count, 20, 'RESTdefaultPageSize is honoured by default (20)' );
227 t::lib::Mocks::mock_preference('RESTdefaultPageSize', 5 );
228 $t->get_ok('/cities')
231 $response_count = scalar @{ $t->tx->res->json };
232 is( $response_count, 5, 'RESTdefaultPageSize is honoured by default (5)' );
234 $t->get_ok('/cities?_page=1&_per_page=-1')
237 $response_count = scalar @{ $t->tx->res->json };
238 is( $response_count, 24, '_per_page=-1 means all resources' );
240 $t->get_ok('/cities?_page=100&_per_page=-1')
243 $response_count = scalar @{ $t->tx->res->json };
244 is( $response_count, 24, 'When _per_page=-1 the page param is not considered' );
246 $schema->storage->txn_rollback;
249 subtest 'objects.search helper, sorting on mapped column' => sub {
253 $schema->storage->txn_begin;
255 # Have complete control over the existing cities to ease testing
256 Koha::Cities->delete;
258 $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'A', city_country => 'Argentina' } });
259 $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'B', city_country => 'Argentina' } });
260 $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'C', city_country => 'Argentina' } });
261 $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'C', city_country => 'Belarus' } });
263 my $t = Test::Mojo->new;
265 $t->get_ok('/cities?_order_by=%2Bname,-country')
269 ->json_is('/0/name' => 'A')
270 ->json_is('/1/name' => 'B')
271 ->json_is('/2/name' => 'C')
272 ->json_is('/2/country' => 'Belarus')
273 ->json_is('/3/name' => 'C')
274 ->json_is('/3/country' => 'Argentina')
277 # Multi-param: traditional
278 $t->get_ok('/cities?_order_by=%2Bname&_order_by=-country')
282 ->json_is('/0/name' => 'A')
283 ->json_is('/1/name' => 'B')
284 ->json_is('/2/name' => 'C')
285 ->json_is('/2/country' => 'Belarus')
286 ->json_is('/3/name' => 'C')
287 ->json_is('/3/country' => 'Argentina')
290 # Multi-param: PHP Style, Passes validation as above, subsequntly explodes
291 $t->get_ok('/cities?_order_by[]=%2Bname&_order_by[]=-country')
295 ->json_is('/0/name' => 'A')
296 ->json_is('/1/name' => 'B')
297 ->json_is('/2/name' => 'C')
298 ->json_is('/2/country' => 'Belarus')
299 ->json_is('/3/name' => 'C')
300 ->json_is('/3/country' => 'Argentina')
304 $t->get_ok('/cities?_order_by=-name')
308 ->json_is('/0/name' => 'C')
309 ->json_is('/1/name' => 'C')
310 ->json_is('/2/name' => 'B')
311 ->json_is('/3/name' => 'A')
314 $schema->storage->txn_rollback;
317 subtest 'objects.search helper, encoding' => sub {
321 $schema->storage->txn_begin;
323 Koha::Cities->delete;
325 $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'A', city_country => 'Argentina' } });
326 $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'B', city_country => '❤Argentina❤' } });
328 my $t = Test::Mojo->new;
329 $t->get_ok('/cities?q={"country": "❤Argentina❤"}')
333 ->json_is('/0/name' => 'B');
335 $schema->storage->txn_rollback;
338 subtest 'objects.search helper, X-Total-Count and X-Base-Total-Count' => sub {
342 $schema->storage->txn_begin;
344 Koha::Cities->delete;
346 my $long_city_name = 'Llanfairpwllgwyngyll';
347 for my $i ( 1 .. length($long_city_name) ) {
348 $builder->build_object(
350 class => 'Koha::Cities',
352 city_name => substr( $long_city_name, 0, $i ),
353 city_country => 'Wales'
359 my $t = Test::Mojo->new;
360 $t->get_ok('/cities?name=L&_per_page=10&_page=1&_match=starts_with')
362 ->header_is( 'X-Total-Count' => 20, 'X-Total-Count header present' )
363 ->header_is( 'X-Base-Total-Count' => 20, 'X-Base-Total-Count header present' );
365 $t->get_ok('/cities?name=Llan&_per_page=10&_page=1&_match=starts_with')
367 ->header_is( 'X-Total-Count' => 17, 'X-Total-Count header present' )
368 ->header_is('X-Base-Total-Count' => 20, 'X-Base-Total-Count header present' );
370 $schema->storage->txn_rollback;
373 subtest 'objects.search helper, embed' => sub {
377 $schema->storage->txn_begin;
379 my $order = $builder->build_object({ class => 'Koha::Acquisition::Orders' });
381 my $t = Test::Mojo->new;
382 $t->get_ok('/orders?order_id=' . $order->ordernumber)
383 ->json_is('/0',$order->to_api({ embed => ( { fund => {} } ) }));
385 $schema->storage->txn_rollback;
388 subtest 'object.search helper with query parameter' => sub {
391 $schema->storage->txn_begin;
393 my $patron1 = $builder->build_object( { class => "Koha::Patrons" } );
394 my $patron2 = $builder->build_object( { class => "Koha::Patrons" } );
395 my $biblio1 = $builder->build_sample_biblio;
396 my $biblio2 = $builder->build_sample_biblio;
397 my $biblio3 = $builder->build_sample_biblio;
398 my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
399 my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
400 my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
402 my $t = Test::Mojo->new;
403 $t->get_ok('/biblios' => json => {"suggestions.suggester.patron_id" => $patron1->borrowernumber })
404 ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
406 $t->get_ok('/biblios' => json => {"suggestions.suggester.patron_id" => $patron2->borrowernumber })
407 ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
409 $schema->storage->txn_rollback;
412 subtest 'object.search helper with q parameter' => sub {
415 $schema->storage->txn_begin;
417 my $patron1 = $builder->build_object( { class => "Koha::Patrons" } );
418 my $patron2 = $builder->build_object( { class => "Koha::Patrons" } );
419 my $biblio1 = $builder->build_sample_biblio;
420 my $biblio2 = $builder->build_sample_biblio;
421 my $biblio3 = $builder->build_sample_biblio;
422 my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
423 my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
424 my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
426 my $t = Test::Mojo->new;
427 $t->get_ok('/biblios?q={"suggestions.suggester.patron_id": "'.$patron1->borrowernumber.'"}')
428 ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
430 $t->get_ok('/biblios?q={"suggestions.suggester.patron_id": "'.$patron2->borrowernumber.'"}')
431 ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
433 $schema->storage->txn_rollback;
436 subtest 'object.search helper with x-koha-query header' => sub {
439 $schema->storage->txn_begin;
441 my $patron1 = $builder->build_object( { class => "Koha::Patrons" } );
442 my $patron2 = $builder->build_object( { class => "Koha::Patrons" } );
443 my $biblio1 = $builder->build_sample_biblio;
444 my $biblio2 = $builder->build_sample_biblio;
445 my $biblio3 = $builder->build_sample_biblio;
446 my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
447 my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
448 my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
450 my $t = Test::Mojo->new;
451 $t->get_ok('/biblios' => {'x-koha-query' => '{"suggestions.suggester.patron_id": "'.$patron1->borrowernumber.'"}'})
452 ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
454 $t->get_ok('/biblios' => {'x-koha-query' => '{"suggestions.suggester.patron_id": "'.$patron2->borrowernumber.'"}'})
455 ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
457 $schema->storage->txn_rollback;
460 subtest 'object.search helper with all query methods' => sub {
463 $schema->storage->txn_begin;
465 my $patron1 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron1'} } );
466 my $patron2 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron2'} } );
467 my $biblio1 = $builder->build_sample_biblio;
468 my $biblio2 = $builder->build_sample_biblio;
469 my $biblio3 = $builder->build_sample_biblio;
470 my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
471 my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
472 my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
474 my $t = Test::Mojo->new;
475 $t->get_ok('/biblios?q={"suggestions.suggester.firstname": "'.$patron1->firstname.'"}' => {'x-koha-query' => '{"suggestions.suggester.patron_id": "'.$patron1->borrowernumber.'"}'} => json => {"suggestions.suggester.cardnumber" => $patron1->cardnumber})
476 ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
478 $t->get_ok('/biblios?q={"suggestions.suggester.firstname": "'.$patron2->firstname.'"}' => {'x-koha-query' => '{"suggestions.suggester.patron_id": "'.$patron2->borrowernumber.'"}'} => json => {"suggestions.suggester.cardnumber" => $patron2->cardnumber})
479 ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
481 $t->get_ok('/biblios?q={"suggestions.suggester.firstname": "'.$patron1->firstname.'"}' => {'x-koha-query' => '{"suggestions.suggester.patron_id": "'.$patron2->borrowernumber.'"}'} => json => {"suggestions.suggester.cardnumber" => $patron2->cardnumber})
482 ->json_is('/count' => 0, 'there shouldn\'t be biblios where suggester has patron1 fistname and patron2 id');
484 $schema->storage->txn_rollback;
487 subtest 'object.search helper order by embedded columns' => sub {
491 $schema->storage->txn_begin;
493 my $patron1 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron1'} } );
494 my $patron2 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron2'} } );
495 my $biblio1 = $builder->build_sample_biblio;
496 my $biblio2 = $builder->build_sample_biblio;
497 my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
498 my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
500 my $t = Test::Mojo->new;
501 $t->get_ok('/biblios?_order_by=-suggestions.suggester.firstname' => json => [{"me.biblio_id" => $biblio1->biblionumber}, {"me.biblio_id" => $biblio2->biblionumber}])
502 ->json_is('/biblios/0/biblio_id' => $biblio2->biblionumber, 'Biblio 2 should be first')
503 ->json_is('/biblios/1/biblio_id' => $biblio1->biblionumber, 'Biblio 1 should be second');
505 $schema->storage->txn_rollback;
508 subtest 'objects.find helper' => sub {
512 my $t = Test::Mojo->new;
514 $schema->storage->txn_begin;
516 my $city_1 = $builder->build_object( { class => 'Koha::Cities' } );
517 my $city_2 = $builder->build_object( { class => 'Koha::Cities' } );
519 $t->get_ok( '/cities/' . $city_1->id )
521 ->json_is( $city_1->to_api );
523 $t->get_ok( '/cities/' . $city_2->id )
525 ->json_is( $city_2->to_api );
528 my $city_2_id = $city_2->id;
530 $t->get_ok( '/cities/' . $city_2_id )
534 $schema->storage->txn_rollback;
537 subtest 'objects.find helper, embed' => sub {
541 my $t = Test::Mojo->new;
543 $schema->storage->txn_begin;
545 my $order = $builder->build_object({ class => 'Koha::Acquisition::Orders' });
547 $t->get_ok( '/orders/' . $order->ordernumber )
548 ->json_is( $order->to_api( { embed => ( { fund => {} } ) } ) );
550 $schema->storage->txn_rollback;
553 subtest 'objects.search helper, public requests' => sub {
557 $schema->storage->txn_begin;
559 my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { branchname => 'A' } });
560 my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { branchname => 'B' } });
562 my $t = Test::Mojo->new;
564 $t->get_ok( '/libraries/'.$library_1->id.'/'.$library_2->id )
565 ->json_is('/0' => $library_1->to_api({ public => 1 }), 'Public representation of $library_1 is retrieved')
566 ->json_is('/1' => $library_2->to_api({ public => 1 }), 'Public representation of $library_2 is retrieved');
568 $schema->storage->txn_rollback;
571 subtest 'objects.search helper, search_limited() tests' => sub {
575 $schema->storage->txn_begin;
577 my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
578 my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
580 my $patron_1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $library_1->id } });
581 my $patron_2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $library_1->id } });
582 my $patron_3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $library_2->id } });
584 my @libraries_where_can_see_patrons = ( $library_1->id, $library_2->id );
586 my $t = Test::Mojo->new;
588 my $mocked_patron = Test::MockModule->new('Koha::Patron');
589 $mocked_patron->mock( 'libraries_where_can_see_patrons', sub
591 return @libraries_where_can_see_patrons;
595 my $patron = $builder->build_object(
597 class => 'Koha::Patrons',
598 value => { flags => 2**4 } # borrowers flag = 4
602 t::lib::Mocks::mock_userenv({ patron => $patron });
604 $t->get_ok( "/my_patrons?q=" . encode_json( { library_id => [ $library_1->id, $library_2->id ] } ) )
606 ->json_is( '/0/patron_id' => $patron_1->id )
607 ->json_is( '/1/patron_id' => $patron_2->id )
608 ->json_is( '/2/patron_id' => $patron_3->id );
610 @libraries_where_can_see_patrons = ( $library_2->id );
612 my $res = $t->get_ok( "/my_patrons?q=" . encode_json( { library_id => [ $library_1->id, $library_2->id ] } ) )
614 ->json_is( '/0/patron_id' => $patron_3->id, 'Returns the only allowed patron' )
617 is( scalar @{$res}, 1, 'Only one patron returned' );
619 $schema->storage->txn_rollback;