Bug 24615: Make object.search helper also order by embedded columns
[koha.git] / t / db_dependent / Koha / REST / Plugin / Objects.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
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.
9 #
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.
14 #
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>.
17
18 use Modern::Perl;
19
20 use Koha::Acquisition::Orders;
21 use Koha::Cities;
22 use Koha::Holds;
23 use Koha::Biblios;
24
25 # Dummy app for testing the plugin
26 use Mojolicious::Lite;
27
28 app->log->level('error');
29
30 plugin 'Koha::REST::Plugin::Objects';
31 plugin 'Koha::REST::Plugin::Query';
32 plugin 'Koha::REST::Plugin::Pagination';
33
34 get '/cities' => sub {
35     my $c = shift;
36     $c->validation->output($c->req->params->to_hash);
37     my $cities = $c->objects->search(Koha::Cities->new);
38     $c->render( status => 200, json => $cities );
39 };
40
41 get '/orders' => sub {
42     my $c = shift;
43     $c->stash('koha.embed', ( { fund => {} } ) );
44     $c->validation->output($c->req->params->to_hash);
45     my $orders = $c->objects->search(Koha::Acquisition::Orders->new);
46     $c->render( status => 200, json => $orders );
47 };
48
49 get '/patrons/:patron_id/holds' => sub {
50     my $c = shift;
51     my $params = $c->req->params->to_hash;
52     $params->{patron_id} = $c->stash("patron_id");
53     $c->validation->output($params);
54     my $holds_set = Koha::Holds->new;
55     my $holds     = $c->objects->search( $holds_set );
56     $c->render( status => 200, json => {count => scalar(@$holds)} );
57 };
58
59 get '/biblios' => sub {
60     my $c = shift;
61     my $output = $c->req->params->to_hash;
62     $output->{query} = $c->req->json if defined $c->req->json;
63     my $headers = $c->req->headers->to_hash;
64     $output->{'x-koha-query'} = $headers->{'x-koha-query'} if defined $headers->{'x-koha-query'};
65     $c->validation->output($output);
66     my $biblios_set = Koha::Biblios->new;
67     $c->stash("koha.embed", {
68         "suggestions" => {
69             children => {
70                 "suggester" => {}
71             }
72         }
73     });
74     my $biblios = $c->objects->search($biblios_set);
75     $c->render( status => 200, json => {count => scalar(@$biblios), biblios => $biblios} );
76 };
77
78
79 # The tests
80 use Test::More tests => 9;
81 use Test::Mojo;
82
83 use t::lib::TestBuilder;
84 use Koha::Database;
85
86 my $t = Test::Mojo->new;
87
88 my $schema  = Koha::Database->new()->schema();
89 my $builder = t::lib::TestBuilder->new;
90
91 subtest 'objects.search helper' => sub {
92
93     plan tests => 38;
94
95     $schema->storage->txn_begin;
96
97     # Remove existing cities to have more control on the search results
98     Koha::Cities->delete;
99
100     # Create three sample cities that match the query. This makes sure we
101     # always have a "next" link regardless of Mojolicious::Plugin::OpenAPI version.
102     $builder->build_object({
103         class => 'Koha::Cities',
104         value => {
105             city_name => 'Manuel'
106         }
107     });
108     $builder->build_object({
109         class => 'Koha::Cities',
110         value => {
111             city_name => 'Manuela'
112         }
113     });
114     $builder->build_object({
115         class => 'Koha::Cities',
116         value => {
117             city_name => 'Manuelab'
118         }
119     });
120
121     $t->get_ok('/cities?name=manuel&_per_page=1&_page=1')
122         ->status_is(200)
123         ->header_like( 'Link' => qr/<http:\/\/.*[\?&]_page=2.*>; rel="next",/ )
124         ->json_has('/0')
125         ->json_hasnt('/1')
126         ->json_is('/0/name' => 'Manuel');
127
128     $builder->build_object({
129         class => 'Koha::Cities',
130         value => {
131             city_name => 'Emanuel'
132         }
133     });
134
135     # _match=starts_with
136     $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=starts_with')
137         ->status_is(200)
138         ->json_has('/0')
139         ->json_has('/1')
140         ->json_has('/2')
141         ->json_hasnt('/3')
142         ->json_is('/0/name' => 'Manuel')
143         ->json_is('/1/name' => 'Manuela')
144         ->json_is('/2/name' => 'Manuelab');
145
146     # _match=ends_with
147     $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=ends_with')
148         ->status_is(200)
149         ->json_has('/0')
150         ->json_has('/1')
151         ->json_hasnt('/2')
152         ->json_is('/0/name' => 'Manuel')
153         ->json_is('/1/name' => 'Emanuel');
154
155     # _match=exact
156     $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=exact')
157         ->status_is(200)
158         ->json_has('/0')
159         ->json_hasnt('/1')
160         ->json_is('/0/name' => 'Manuel');
161
162     # _match=contains
163     $t->get_ok('/cities?name=manuel&_per_page=4&_page=1&_match=contains')
164         ->status_is(200)
165         ->json_has('/0')
166         ->json_has('/1')
167         ->json_has('/2')
168         ->json_has('/3')
169         ->json_hasnt('/4')
170         ->json_is('/0/name' => 'Manuel')
171         ->json_is('/1/name' => 'Manuela')
172         ->json_is('/2/name' => 'Manuelab')
173         ->json_is('/3/name' => 'Emanuel');
174
175     $schema->storage->txn_rollback;
176 };
177
178 subtest 'objects.search helper, sorting on mapped column' => sub {
179
180     plan tests => 14;
181
182     $schema->storage->txn_begin;
183
184     # Have complete control over the existing cities to ease testing
185     Koha::Cities->delete;
186
187     $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'A', city_country => 'Argentina' } });
188     $builder->build_object({ class => 'Koha::Cities', value => { city_name => 'B', city_country => 'Argentina' } });
189
190     $t->get_ok('/cities?_order_by=%2Bname&_order_by=+country')
191       ->status_is(200)
192       ->json_has('/0')
193       ->json_has('/1')
194       ->json_hasnt('/2')
195       ->json_is('/0/name' => 'A')
196       ->json_is('/1/name' => 'B');
197
198     $t->get_ok('/cities?_order_by=-name')
199       ->status_is(200)
200       ->json_has('/0')
201       ->json_has('/1')
202       ->json_hasnt('/2')
203       ->json_is('/0/name' => 'B')
204       ->json_is('/1/name' => 'A');
205
206     $schema->storage->txn_rollback;
207 };
208
209 subtest 'objects.search helper, embed' => sub {
210
211     plan tests => 2;
212
213     $schema->storage->txn_begin;
214
215     my $order = $builder->build_object({ class => 'Koha::Acquisition::Orders' });
216
217     $t->get_ok('/orders?order_id=' . $order->ordernumber)
218       ->json_is('/0',$order->to_api({ embed => ( { fund => {} } ) }));
219
220     $schema->storage->txn_rollback;
221 };
222
223 subtest 'objects.search helper, with path parameters and _match' => sub {
224     plan tests => 8;
225
226     $schema->storage->txn_begin;
227
228     Koha::Holds->search()->delete;
229
230     my $patron = Koha::Patrons->find(10);
231     $patron->delete if $patron;
232     $patron = $builder->build_object( { class => "Koha::Patrons" } );
233     $patron->borrowernumber(10)->store;
234     $builder->build_object(
235         {
236             class => "Koha::Holds",
237             value => { borrowernumber => $patron->borrowernumber }
238         }
239     );
240
241     $t->get_ok('/patrons/1/holds?_match=exact')
242       ->json_is('/count' => 0, 'there should be no holds for borrower 1 with _match=exact');
243
244     $t->get_ok('/patrons/1/holds?_match=contains')
245       ->json_is('/count' => 0, 'there should be no holds for borrower 1 with _match=contains');
246
247     $t->get_ok('/patrons/10/holds?_match=exact')
248       ->json_is('/count' => 1, 'there should be 1 hold for borrower 10 with _match=exact');
249
250     $t->get_ok('/patrons/10/holds?_match=contains')
251       ->json_is('/count' => 1, 'there should be 1 hold for borrower 10 with _match=contains');
252
253     $schema->storage->txn_rollback;
254 };
255
256 subtest 'object.search helper with query parameter' => sub {
257     plan tests => 4;
258
259     $schema->storage->txn_begin;
260
261     my $patron1 = $builder->build_object( { class => "Koha::Patrons" } );
262     my $patron2 = $builder->build_object( { class => "Koha::Patrons" } );
263     my $biblio1 = $builder->build_sample_biblio;
264     my $biblio2 = $builder->build_sample_biblio;
265     my $biblio3 = $builder->build_sample_biblio;
266     my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
267     my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
268     my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
269
270     $t->get_ok('/biblios' => json => {"suggestions.suggester.patron_id" => $patron1->borrowernumber })
271       ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
272
273     $t->get_ok('/biblios' => json => {"suggestions.suggester.patron_id" => $patron2->borrowernumber })
274       ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
275
276     $schema->storage->txn_rollback;
277 };
278
279 subtest 'object.search helper with q parameter' => sub {
280     plan tests => 4;
281
282     $schema->storage->txn_begin;
283
284     my $patron1 = $builder->build_object( { class => "Koha::Patrons" } );
285     my $patron2 = $builder->build_object( { class => "Koha::Patrons" } );
286     my $biblio1 = $builder->build_sample_biblio;
287     my $biblio2 = $builder->build_sample_biblio;
288     my $biblio3 = $builder->build_sample_biblio;
289     my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
290     my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
291     my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
292
293     $t->get_ok('/biblios?q={"suggestions.suggester.patron_id": "'.$patron1->borrowernumber.'"}')
294       ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
295
296     $t->get_ok('/biblios?q={"suggestions.suggester.patron_id": "'.$patron2->borrowernumber.'"}')
297       ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
298
299     $schema->storage->txn_rollback;
300 };
301
302 subtest 'object.search helper with x-koha-query header' => sub {
303     plan tests => 4;
304
305     $schema->storage->txn_begin;
306
307     my $patron1 = $builder->build_object( { class => "Koha::Patrons" } );
308     my $patron2 = $builder->build_object( { class => "Koha::Patrons" } );
309     my $biblio1 = $builder->build_sample_biblio;
310     my $biblio2 = $builder->build_sample_biblio;
311     my $biblio3 = $builder->build_sample_biblio;
312     my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
313     my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
314     my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
315
316     $t->get_ok('/biblios' => {'x-koha-query' => '{"suggestions.suggester.patron_id": "'.$patron1->borrowernumber.'"}'})
317       ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
318
319     $t->get_ok('/biblios' => {'x-koha-query' => '{"suggestions.suggester.patron_id": "'.$patron2->borrowernumber.'"}'})
320       ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
321
322     $schema->storage->txn_rollback;
323 };
324
325 subtest 'object.search helper with all query methods' => sub {
326     plan tests => 6;
327
328     $schema->storage->txn_begin;
329
330     my $patron1 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron1'} } );
331     my $patron2 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron2'} } );
332     my $biblio1 = $builder->build_sample_biblio;
333     my $biblio2 = $builder->build_sample_biblio;
334     my $biblio3 = $builder->build_sample_biblio;
335     my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
336     my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
337     my $suggestion3 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio3->biblionumber} } );
338
339     $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})
340       ->json_is('/count' => 1, 'there should be 1 biblio with suggestions of patron 1');
341
342     $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})
343       ->json_is('/count' => 2, 'there should be 2 biblios with suggestions of patron 2');
344
345     $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})
346       ->json_is('/count' => 0, 'there shouldn\'t be biblios where suggester has patron1 fistname and patron2 id');
347
348     $schema->storage->txn_rollback;
349 };
350
351 subtest 'object.search helper order by embedded columns' => sub {
352     plan tests => 3;
353
354     my $patron1 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron1'} } );
355     my $patron2 = $builder->build_object( { class => "Koha::Patrons" , value => {firstname=>'patron2'} } );
356     my $biblio1 = $builder->build_sample_biblio;
357     my $biblio2 = $builder->build_sample_biblio;
358     my $suggestion1 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron1->borrowernumber, biblionumber => $biblio1->biblionumber} } );
359     my $suggestion2 = $builder->build_object( { class => "Koha::Suggestions", value => { suggestedby => $patron2->borrowernumber, biblionumber => $biblio2->biblionumber} } );
360
361     $t->get_ok('/biblios?_order_by=-suggestions.suggester.firstname' => json => [{"me.biblio_id" => $biblio1->biblionumber}, {"me.biblio_id" => $biblio2->biblionumber}])
362       ->json_is('/biblios/0/biblio_id' => $biblio2->biblionumber, 'Biblio 2 should be first')
363       ->json_is('/biblios/1/biblio_id' => $biblio1->biblionumber, 'Biblio 1 should be second');
364
365     $schema->storage->txn_begin;
366 }