Bug 18403: Patron modification requests
[koha.git] / t / db_dependent / Koha / Patron / Modifications.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 utf8;
21
22 use Test::More tests => 6;
23 use Test::Exception;
24
25 use t::lib::TestBuilder;
26
27 use Digest::MD5 qw( md5_base64 md5_hex );
28 use Try::Tiny;
29
30 use C4::Context;
31 use C4::Members;
32 use C4::Members::Attributes qw( GetBorrowerAttributes );
33 use Koha::Patrons;
34 use Koha::Patron::Attribute;
35
36 BEGIN {
37     use_ok('Koha::Patron::Modification');
38     use_ok('Koha::Patron::Modifications');
39 }
40
41 my $schema  = Koha::Database->new->schema;
42 my $builder = t::lib::TestBuilder->new;
43
44 subtest 'new() tests' => sub {
45
46     plan tests => 3;
47
48     $schema->storage->txn_begin;
49
50     Koha::Patron::Modifications->search->delete;
51
52     # Create new pending modification
53     Koha::Patron::Modification->new(
54         {   verification_token => '1234567890',
55             surname            => 'Hall',
56             firstname          => 'Kyle'
57         }
58     )->store();
59
60     ## Get the new pending modification
61     my $borrower = Koha::Patron::Modifications->find(
62         { verification_token => '1234567890' } );
63
64     ## Verify we get the same data
65     is( $borrower->surname, 'Hall',
66         'Found modification has matching surname' );
67
68     throws_ok {
69         Koha::Patron::Modification->new(
70             {   verification_token => '1234567890',
71                 surname            => 'Hall',
72                 firstname          => 'Daria'
73             }
74         )->store();
75     }
76     'Koha::Exceptions::Patron::Modification::DuplicateVerificationToken',
77         'Attempting to add a duplicate verification raises the correct exception';
78     is( $@,
79         'Duplicate verification token 1234567890',
80         'Exception carries the right message'
81     );
82
83     $schema->storage->txn_rollback;
84 };
85
86 subtest 'store( extended_attributes ) tests' => sub {
87
88     plan tests => 4;
89
90     $schema->storage->txn_begin;
91
92     Koha::Patron::Modifications->search->delete;
93
94     my $patron
95         = $builder->build( { source => 'Borrower' } )->{borrowernumber};
96     my $verification_token = md5_hex( time().{}.rand().{}.$$ );
97     my $valid_json_text    = '[{"code":"CODE","value":"VALUE"}]';
98     my $invalid_json_text  = '[{"code":"CODE";"value":"VALUE"}]';
99
100     Koha::Patron::Modification->new(
101         {   verification_token  => $verification_token,
102             borrowernumber      => $patron,
103             surname             => 'Hall',
104             extended_attributes => $valid_json_text
105         }
106     )->store();
107
108     my $patron_modification
109         = Koha::Patron::Modifications->search( { borrowernumber => $patron } )
110         ->next;
111
112     is( $patron_modification->surname,
113         'Hall', 'Patron modification correctly stored with valid JSON data' );
114     is( $patron_modification->extended_attributes,
115         $valid_json_text,
116         'Patron modification correctly stored with valid JSON data' );
117
118     $verification_token = md5_hex( time().{}.rand().{}.$$ );
119     throws_ok {
120         Koha::Patron::Modification->new(
121             {   verification_token  => $verification_token,
122                 borrowernumber      => $patron,
123                 surname             => 'Hall',
124                 extended_attributes => $invalid_json_text
125             }
126         )->store();
127     }
128     'Koha::Exceptions::Patron::Modification::InvalidData',
129         'Trying to store invalid JSON in extended_attributes field raises exception';
130
131     is( $@, 'The passed extended_attributes is not valid JSON' );
132
133     $schema->storage->txn_rollback;
134 };
135
136 subtest 'approve tests' => sub {
137
138     plan tests => 20;
139
140     $schema->storage->txn_begin;
141
142     Koha::Patron::Modifications->search->delete;
143
144     my $patron_hashref = $builder->build( { source => 'Borrower' } );
145     $builder->build(
146         { source => 'BorrowerAttributeType', value => { code => 'CODE_1' } }
147     );
148     $builder->build(
149         { source => 'BorrowerAttributeType', value => { code => 'CODE_2' } }
150     );
151     my $verification_token = md5_hex( time().{}.rand().{}.$$ );
152     my $valid_json_text
153         = '[{"code":"CODE_1","value":"VALUE_1"},{"code":"CODE_2","value":0}]';
154     my $patron_modification = Koha::Patron::Modification->new(
155         {   borrowernumber      => $patron_hashref->{borrowernumber},
156             firstname           => 'Kyle',
157             verification_token  => $verification_token,
158             extended_attributes => $valid_json_text
159         }
160     )->store();
161
162     ok( $patron_modification->approve,
163         'Patron modification correctly approved' );
164     my $patron = Koha::Patrons->find( $patron_hashref->{borrowernumber} );
165     isnt(
166         $patron->firstname,
167         $patron_hashref->{firstname},
168         'Patron modification changed firstname'
169     );
170     is( $patron->firstname, 'Kyle',
171         'Patron modification set the right firstname' );
172     my @patron_attributes = GetBorrowerAttributes( $patron->borrowernumber );
173     is( $patron_attributes[0][0]->{code},
174         'CODE_1', 'Patron modification correctly saved attribute code' );
175     is( $patron_attributes[0][0]->{value},
176         'VALUE_1', 'Patron modification correctly saved attribute value' );
177     is( $patron_attributes[0][1]->{code},
178         'CODE_2', 'Patron modification correctly saved attribute code' );
179     is( $patron_attributes[0][1]->{value},
180         0, 'Patron modification correctly saved attribute with value 0, not confused with delete' );
181
182     # Create a new Koha::Patron::Modification, skip extended_attributes to
183     # bypass checks
184     $patron_modification = Koha::Patron::Modification->new(
185         {   borrowernumber     => $patron_hashref->{borrowernumber},
186             firstname          => 'Kylie',
187             verification_token => $verification_token
188         }
189     )->store();
190
191     # Add invalid JSON to extended attributes
192     $patron_modification->extended_attributes(
193         '[{"code":"CODE";"values:VALUES"}]');
194     throws_ok { $patron_modification->approve }
195     'Koha::Exceptions::Patron::Modification::InvalidData',
196         'The right exception is thrown if invalid data is on extended_attributes';
197
198     $patron = Koha::Patrons->find( $patron_hashref->{borrowernumber} );
199     isnt( $patron->firstname, 'Kylie', 'Patron modification didn\'t apply' );
200
201     # Try changing only a portion of the attributes
202     my $bigger_json
203         = '[{"code":"CODE_2","value":"Tomasito"},{"code":"CODE_2","value":"None"}]';
204     $verification_token = md5_hex( time() . {} . rand() . {} . $$ );
205
206     $patron_modification = Koha::Patron::Modification->new(
207         {   borrowernumber      => $patron->borrowernumber,
208             extended_attributes => $bigger_json,
209             verification_token  => $verification_token
210         }
211     )->store();
212     ok( $patron_modification->approve,
213         'Patron modification correctly approved' );
214     @patron_attributes
215         = map { $_->unblessed }
216         Koha::Patron::Attributes->search(
217         { borrowernumber => $patron->borrowernumber } );
218
219     is( $patron_attributes[0]->{code},
220         'CODE_1', 'Untouched attribute type is preserved (code)' );
221     is( $patron_attributes[0]->{attribute},
222         'VALUE_1', 'Untouched attribute type is preserved (attribute)' );
223
224     is( $patron_attributes[1]->{code},
225         'CODE_2', 'Attribute updated correctly (code)' );
226     is( $patron_attributes[1]->{attribute},
227         'Tomasito', 'Attribute updated correctly (attribute)' );
228
229     is( $patron_attributes[2]->{code},
230         'CODE_2', 'Attribute updated correctly (code)' );
231     is( $patron_attributes[2]->{attribute},
232         'None', 'Attribute updated correctly (attribute)' );
233
234     my $empty_code_json = '[{"code":"CODE_2","value":""}]';
235     $verification_token = md5_hex( time() . {} . rand() . {} . $$ );
236
237     $patron_modification = Koha::Patron::Modification->new(
238         {   borrowernumber      => $patron->borrowernumber,
239             extended_attributes => $empty_code_json,
240             verification_token  => $verification_token
241         }
242     )->store();
243     ok( $patron_modification->approve,
244         'Patron modification correctly approved' );
245     @patron_attributes
246         = map { $_->unblessed }
247         Koha::Patron::Attributes->search(
248         { borrowernumber => $patron->borrowernumber } );
249
250     is( $patron_attributes[0]->{code},
251         'CODE_1', 'Untouched attribute type is preserved (code)' );
252     is( $patron_attributes[0]->{attribute},
253         'VALUE_1', 'Untouched attribute type is preserved (attribute)' );
254
255     my $count = Koha::Patron::Attributes->search({ borrowernumber => $patron->borrowernumber, code => 'CODE_2' })->count;
256     is( $count, 0, 'Attributes deleted when modification contained an empty one');
257
258     $schema->storage->txn_rollback;
259 };
260
261 subtest 'pending_count() and pending() tests' => sub {
262
263     plan tests => 16;
264
265     $schema->storage->txn_begin;
266
267     Koha::Patron::Modifications->search->delete;
268     my $library_1 = $builder->build( { source => 'Branch' } )->{branchcode};
269     my $library_2 = $builder->build( { source => 'Branch' } )->{branchcode};
270     $builder->build({ source => 'BorrowerAttributeType', value => { code => 'CODE_1' } });
271     $builder->build({ source => 'BorrowerAttributeType', value => { code => 'CODE_2', repeatable => 1 } });
272
273     my $patron_1
274         = $builder->build(
275         { source => 'Borrower', value => { branchcode => $library_1 } } );
276     my $patron_2
277         = $builder->build(
278         { source => 'Borrower', value => { branchcode => $library_2 } } );
279     my $patron_3
280         = $builder->build(
281         { source => 'Borrower', value => { branchcode => $library_2 } } );
282     $patron_1 = Koha::Patrons->find( $patron_1->{borrowernumber} );
283     $patron_2 = Koha::Patrons->find( $patron_2->{borrowernumber} );
284     $patron_3 = Koha::Patrons->find( $patron_3->{borrowernumber} );
285     my $verification_token_1 = md5_hex( time().{}.rand().{}.$$ );
286     my $verification_token_2 = md5_hex( time().{}.rand().{}.$$ );
287     my $verification_token_3 = md5_hex( time().{}.rand().{}.$$ );
288
289     Koha::Patron::Attribute->new({ borrowernumber => $patron_1->borrowernumber, code => 'CODE_1', attribute => 'hello' } )->store();
290     Koha::Patron::Attribute->new({ borrowernumber => $patron_2->borrowernumber, code => 'CODE_2', attribute => 'bye' } )->store();
291
292     my $modification_1 = Koha::Patron::Modification->new(
293         {   borrowernumber     => $patron_1->borrowernumber,
294             surname            => 'Hall',
295             firstname          => 'Kyle',
296             verification_token => $verification_token_1,
297             extended_attributes => '[{"code":"CODE_1","value":""}]'
298         }
299     )->store();
300
301     is( Koha::Patron::Modifications->pending_count,
302         1, 'pending_count() correctly returns 1' );
303
304     my $modification_2 = Koha::Patron::Modification->new(
305         {   borrowernumber     => $patron_2->borrowernumber,
306             surname            => 'Smith',
307             firstname          => 'Sandy',
308             verification_token => $verification_token_2,
309             extended_attributes => '[{"code":"CODE_2","value":"año"},{"code":"CODE_2","value":"ciao"}]'
310         }
311     )->store();
312
313     my $modification_3 = Koha::Patron::Modification->new(
314         {   borrowernumber     => $patron_3->borrowernumber,
315             surname            => 'Smithy',
316             firstname          => 'Sandy',
317             verification_token => $verification_token_3
318         }
319     )->store();
320
321     is( Koha::Patron::Modifications->pending_count,
322         3, 'pending_count() correctly returns 3' );
323
324     my $pending = Koha::Patron::Modifications->pending();
325     is( scalar @{$pending}, 3, 'pending() returns an array with 3 elements' );
326
327     my @filtered_modifications = grep { $_->{borrowernumber} eq $patron_1->borrowernumber } @{$pending};
328     my $p1_pm = $filtered_modifications[0];
329     my $p1_pm_attribute_1 = $p1_pm->{extended_attributes}->[0];
330
331     is( scalar @{$p1_pm->{extended_attributes}}, 1, 'patron 1 has modification has one pending attribute modification' );
332     is( ref($p1_pm_attribute_1), 'Koha::Patron::Attribute', 'patron modification has single attribute object' );
333     is( $p1_pm_attribute_1->attribute, '', 'patron 1 has an empty value for the attribute' );
334
335     @filtered_modifications = grep { $_->{borrowernumber} eq $patron_2->borrowernumber } @{$pending};
336     my $p2_pm = $filtered_modifications[0];
337
338     is( scalar @{$p2_pm->{extended_attributes}}, 2 , 'patron 2 has 2 attribute modifications' );
339
340     my $p2_pm_attribute_1 = $p2_pm->{extended_attributes}->[0];
341     my $p2_pm_attribute_2 = $p2_pm->{extended_attributes}->[1];
342
343     is( ref($p2_pm_attribute_1), 'Koha::Patron::Attribute', 'patron modification has single attribute object' );
344     is( ref($p2_pm_attribute_2), 'Koha::Patron::Attribute', 'patron modification has single attribute object' );
345
346     is( $p2_pm_attribute_1->attribute, 'año', 'patron modification has the right attribute change' );
347     is( $p2_pm_attribute_2->attribute, 'ciao', 'patron modification has the right attribute change' );
348
349
350     C4::Context->_new_userenv('xxx');
351     set_logged_in_user( $patron_1 );
352     is( Koha::Patron::Modifications->pending_count($library_1),
353         1, 'pending_count() correctly returns 1 if filtered by library' );
354
355     is( Koha::Patron::Modifications->pending_count($library_2),
356         2, 'pending_count() correctly returns 2 if filtered by library' );
357
358     $modification_1->approve;
359
360     is( Koha::Patron::Modifications->pending_count,
361         2, 'pending_count() correctly returns 2' );
362
363     $modification_2->approve;
364
365     is( Koha::Patron::Modifications->pending_count,
366         1, 'pending_count() correctly returns 1' );
367
368     $modification_3->approve;
369
370     is( Koha::Patron::Modifications->pending_count,
371         0, 'pending_count() correctly returns 0' );
372
373     $schema->storage->txn_rollback;
374 };
375
376 sub set_logged_in_user {
377     my ($patron) = @_;
378     C4::Context->set_userenv(
379         $patron->borrowernumber, $patron->userid,
380         $patron->cardnumber,     'firstname',
381         'surname',               $patron->library->branchcode,
382         'Midway Public Library', $patron->flags,
383         '',                      ''
384     );
385 }