Bug 31202: Don't remove optional SIP fields with a value of "0"
[koha.git] / t / db_dependent / SIP / Message.t
1 #!/usr/bin/perl
2
3 # Tests for SIP::Sip::MsgType
4 # Please help to extend it!
5
6 # This file is part of Koha.
7 #
8 # Copyright 2016 Rijksmuseum
9 #
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22
23 use Modern::Perl;
24 use Test::More tests => 13;
25 use Test::Exception;
26 use Test::MockObject;
27 use Test::MockModule;
28 use Test::Warn;
29
30 use t::lib::Mocks;
31 use t::lib::TestBuilder;
32
33 use C4::Reserves qw( AddReserve );
34 use C4::Circulation qw( AddReturn );
35 use Koha::Database;
36 use Koha::AuthUtils qw(hash_password);
37 use Koha::DateUtils qw( dt_from_string output_pref );
38 use Koha::Items;
39 use Koha::Checkouts;
40 use Koha::Old::Checkouts;
41 use Koha::Patrons;
42 use Koha::Holds;
43
44 use C4::SIP::ILS;
45 use C4::SIP::ILS::Patron;
46 use C4::SIP::Sip qw(write_msg);
47 use C4::SIP::Sip::Constants qw(:all);
48 use C4::SIP::Sip::MsgType;
49
50 use constant PATRON_PW => 'do_not_ever_use_this_one';
51
52 # START testing
53 subtest 'Testing Patron Status Request V2' => sub {
54     my $schema = Koha::Database->new->schema;
55     $schema->storage->txn_begin;
56     plan tests => 13;
57     $C4::SIP::Sip::protocol_version = 2;
58     test_request_patron_status_v2();
59     $schema->storage->txn_rollback;
60 };
61
62 subtest 'Testing Patron Info Request V2' => sub {
63     my $schema = Koha::Database->new->schema;
64     $schema->storage->txn_begin;
65     plan tests => 24;
66     $C4::SIP::Sip::protocol_version = 2;
67     test_request_patron_info_v2();
68     $schema->storage->txn_rollback;
69 };
70
71 subtest 'Checkout V2' => sub {
72     my $schema = Koha::Database->new->schema;
73     $schema->storage->txn_begin;
74     plan tests => 5;
75     $C4::SIP::Sip::protocol_version = 2;
76     test_checkout_v2();
77     $schema->storage->txn_rollback;
78 };
79
80 subtest 'Test checkout desensitize' => sub {
81     my $schema = Koha::Database->new->schema;
82     $schema->storage->txn_begin;
83     plan tests => 3;
84     $C4::SIP::Sip::protocol_version = 2;
85     test_checkout_desensitize();
86     $schema->storage->txn_rollback;
87 };
88
89 subtest 'Test renew desensitize' => sub {
90     my $schema = Koha::Database->new->schema;
91     $schema->storage->txn_begin;
92     plan tests => 3;
93     $C4::SIP::Sip::protocol_version = 2;
94     test_renew_desensitize();
95     $schema->storage->txn_rollback;
96 };
97
98 subtest 'Checkin V2' => sub {
99     my $schema = Koha::Database->new->schema;
100     $schema->storage->txn_begin;
101     plan tests => 39;
102     $C4::SIP::Sip::protocol_version = 2;
103     test_checkin_v2();
104     $schema->storage->txn_rollback;
105 };
106
107 subtest 'Test hold_patron_bcode' => sub {
108     my $schema = Koha::Database->new->schema;
109     $schema->storage->txn_begin;
110     plan tests => 2;
111     $C4::SIP::Sip::protocol_version = 2;
112     test_hold_patron_bcode();
113     $schema->storage->txn_rollback;
114 };
115
116 subtest 'hold_patron_name() tests' => sub {
117
118     plan tests => 3;
119
120     my $schema = Koha::Database->new->schema;
121     $schema->storage->txn_begin;
122
123     my $builder = t::lib::TestBuilder->new();
124
125     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
126     my ( $response, $findpatron );
127     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
128
129     my $item = $builder->build_sample_item(
130         {
131             damaged       => 0,
132             withdrawn     => 0,
133             itemlost      => 0,
134             restricted    => 0,
135             homebranch    => $branchcode,
136             holdingbranch => $branchcode
137         }
138     );
139
140     my $server = { ils => $mocks->{ils} };
141     my $sip_item = C4::SIP::ILS::Item->new( $item->barcode );
142
143     is( $sip_item->hold_patron_name, q{}, "SIP item with no hold returns empty string for patron name" );
144
145     my $resp = C4::SIP::Sip::maybe_add( FID_CALL_NUMBER, $sip_item->hold_patron_name, $server );
146     is( $resp, q{}, "maybe_add returns empty string for SIP item with no hold returns empty string" );
147
148     $resp = C4::SIP::Sip::maybe_add( FID_CALL_NUMBER, "0", $server );
149     is( $resp, q{CS0|}, "maybe_add will create the field of the string '0'" );
150
151     $schema->storage->txn_rollback;
152 };
153
154 subtest 'Lastseen response' => sub {
155
156     my $schema = Koha::Database->new->schema;
157     $schema->storage->txn_begin;
158
159     plan tests => 6;
160     my $builder = t::lib::TestBuilder->new();
161     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
162     my ( $response, $findpatron );
163     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
164     my $seen_patron = $builder->build({
165         source => 'Borrower',
166         value  => {
167             lastseen => '2001-01-01',
168             password => hash_password( PATRON_PW ),
169             branchcode => $branchcode,
170         },
171     });
172     my $cardnum = $seen_patron->{cardnumber};
173     my $sip_patron = C4::SIP::ILS::Patron->new( $cardnum );
174     $findpatron = $sip_patron;
175
176     my $siprequest = PATRON_INFO. 'engYYYYMMDDZZZZHHMMSS'.'Y         '.
177         FID_INST_ID. $branchcode. '|'.
178         FID_PATRON_ID. $cardnum. '|'.
179         FID_PATRON_PWD. PATRON_PW. '|';
180     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
181
182     my $server = { ils => $mocks->{ils} };
183     undef $response;
184     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '' );
185     $msg->handle_patron_info( $server );
186
187     isnt( $response, undef, 'At least we got a response.' );
188     my $respcode = substr( $response, 0, 2 );
189     is( $respcode, PATRON_INFO_RESP, 'Response code fine' );
190     $seen_patron = Koha::Patrons->find({ cardnumber => $seen_patron->{cardnumber} });
191     is( output_pref({str => $seen_patron->lastseen(), dateonly => 1}), output_pref({str => '2001-01-01', dateonly => 1}),'Last seen not updated if not tracking patrons');
192     undef $response;
193     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '1' );
194     $msg->handle_patron_info( $server );
195
196     isnt( $response, undef, 'At least we got a response.' );
197     $respcode = substr( $response, 0, 2 );
198     is( $respcode, PATRON_INFO_RESP, 'Response code fine' );
199     $seen_patron = Koha::Patrons->find({ cardnumber => $seen_patron->cardnumber() });
200     is( output_pref({str => $seen_patron->lastseen(), dateonly => 1}), output_pref({dt => dt_from_string(), dateonly => 1}),'Last seen updated if tracking patrons');
201     $schema->storage->txn_rollback;
202
203 };
204
205 subtest "Test build_additional_item_fields_string" => sub {
206     my $schema = Koha::Database->new->schema;
207     $schema->storage->txn_begin;
208
209     plan tests => 2;
210
211     my $builder = t::lib::TestBuilder->new();
212
213     my $item = $builder->build_sample_item;
214     my $ils_item = C4::SIP::ILS::Item->new( $item->barcode );
215
216     my $server = {};
217     $server->{account}->{item_field}->{code} = 'itemnumber';
218     $server->{account}->{item_field}->{field} = 'XY';
219     my $attribute_string = $ils_item->build_additional_item_fields_string( $server );
220     is( $attribute_string, "XY".$item->itemnumber."|", 'Attribute field generated correctly with single param' );
221
222     $server = {};
223     $server->{account}->{item_field}->[0]->{code} = 'itemnumber';
224     $server->{account}->{item_field}->[0]->{field} = 'XY';
225     $server->{account}->{item_field}->[1]->{code} = 'biblionumber';
226     $server->{account}->{item_field}->[1]->{field} = 'YZ';
227     $attribute_string = $ils_item->build_additional_item_fields_string( $server );
228     is( $attribute_string, sprintf("XY%s|YZ%s|", $item->itemnumber, $item->biblionumber), 'Attribute field generated correctly with multiple params' );
229
230     $schema->storage->txn_rollback;
231 };
232
233 subtest "Test cr_item_field" => sub {
234     plan tests => 2;
235
236     my $builder = t::lib::TestBuilder->new();
237     my $branchcode  = $builder->build({ source => 'Branch' })->{branchcode};
238     my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode};
239     my ( $response, $findpatron );
240     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
241
242     # create some data
243     my $patron1 = $builder->build({
244         source => 'Borrower',
245         value  => {
246             password => hash_password( PATRON_PW ),
247         },
248     });
249     my $card1 = $patron1->{cardnumber};
250     my $sip_patron1 = C4::SIP::ILS::Patron->new( $card1 );
251     $findpatron = $sip_patron1;
252     my $item_object = $builder->build_sample_item({
253         damaged => 0,
254         withdrawn => 0,
255         itemlost => 0,
256         restricted => 0,
257         homebranch => $branchcode,
258         holdingbranch => $branchcode,
259     });
260
261     my $mockILS = $mocks->{ils};
262     my $server = { ils => $mockILS, account => {} };
263     $mockILS->mock( 'institution', sub { $branchcode; } );
264     $mockILS->mock( 'supports', sub { return; } );
265     $mockILS->mock( 'checkin', sub {
266         shift;
267         return C4::SIP::ILS->checkin(@_);
268     });
269     my $today = dt_from_string;
270
271     my $respcode;
272
273     # Not checked out, toggle option checked_in_ok
274     my $siprequest = CHECKIN . 'N' . 'YYYYMMDDZZZZHHMMSS' .
275         siprequestdate( $today->clone->add( days => 1) ) .
276         FID_INST_ID . $branchcode . '|'.
277         FID_ITEM_ID . $item_object->barcode . '|' .
278         FID_TERMINAL_PWD . 'ignored' . '|';
279     undef $response;
280     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
281
282     $server->{account}->{cr_item_field} = 'itemnumber';
283
284     $msg->handle_checkin( $server );
285
286     my $id = $item_object->id;
287     ok( $response =~ m/CR$id/, "Found correct CR field in response");
288
289     $siprequest = ITEM_INFORMATION . 'YYYYMMDDZZZZHHMMSS' .
290         FID_INST_ID . $branchcode . '|'.
291         FID_ITEM_ID . $item_object->barcode . '|' .
292         FID_TERMINAL_PWD . 'ignored' . '|';
293     undef $response;
294     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
295
296     $mockILS->mock( 'find_item', sub {
297         return C4::SIP::ILS::Item->new( $item_object->barcode );
298     });
299
300     $server->{account}->{cr_item_field} = 'itype';
301
302     $msg->handle_item_information( $server );
303
304     my $itype = $item_object->itype;
305     ok( $response =~ m/CR$itype/, "Found correct CR field in response");
306 };
307
308 subtest 'Patron info summary > 5 should not crash server' => sub {
309
310     my $schema = Koha::Database->new->schema;
311     $schema->storage->txn_begin;
312
313     plan tests => 22;
314     my $builder = t::lib::TestBuilder->new();
315     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
316     my ( $response, $findpatron );
317     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
318     my $seen_patron = $builder->build({
319         source => 'Borrower',
320         value  => {
321             lastseen => '2001-01-01',
322             password => hash_password( PATRON_PW ),
323             branchcode => $branchcode,
324         },
325     });
326     my $cardnum = $seen_patron->{cardnumber};
327     my $sip_patron = C4::SIP::ILS::Patron->new( $cardnum );
328     $findpatron = $sip_patron;
329
330     my @summaries = (
331         '          ',
332         'Y         ',
333         ' Y        ',
334         '  Y       ',
335         '   Y      ',
336         '    Y     ',
337         '     Y    ',
338         '      Y   ',
339         '       Y  ',
340         '        Y ',
341         '         Y',
342     );
343     for my $summary ( @summaries ) {
344         my $siprequest = PATRON_INFO . 'engYYYYMMDDZZZZHHMMSS' . $summary .
345             FID_INST_ID . $branchcode . '|' .
346             FID_PATRON_ID . $cardnum . '|' .
347             FID_PATRON_PWD . PATRON_PW . '|';
348         my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
349
350         my $server = { ils => $mocks->{ils} };
351         undef $response;
352         $msg->handle_patron_info( $server );
353
354         isnt( $response, undef, 'At least we got a response.' );
355         my $respcode = substr( $response, 0, 2 );
356         is( $respcode, PATRON_INFO_RESP, 'Response code fine' );
357     }
358
359     $schema->storage->txn_rollback;
360 };
361
362 subtest 'SC status tests' => sub {
363
364     my $schema = Koha::Database->new->schema;
365     $schema->storage->txn_begin;
366
367     plan tests => 2;
368
369     my $builder = t::lib::TestBuilder->new();
370     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
371     my $sip_user = $builder->build_object({ class => "Koha::Patrons" });
372
373     my ( $response, $findpatron );
374     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
375     my $mockILS = $mocks->{ils};
376     $mockILS->mock( 'checkout_ok', sub {1} );
377     $mockILS->mock( 'checkin_ok', sub {1} );
378     $mockILS->mock( 'status_update_ok', sub {1} );
379     $mockILS->mock( 'offline_ok', sub {1} );
380     $mockILS->mock( 'supports', sub {1} );
381     my $server = Test::MockObject->new();
382     $server->mock( 'get_timeout', sub {'100'});
383     $server->{ils} = $mockILS;
384     $server->{sip_username} = $sip_user->userid;
385     $server->{account} = {};
386     $server->{policy} = { renewal =>1,retries=>'000'};
387
388     my $siprequest = SC_STATUS . '0' . '030' . '2.00';
389     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
390     $msg->handle_sc_status( $server );
391
392     like( $response, qr/98YYYYYY100000[0-9 ]{19}.00AO|BXYYYYYYYYYYYYYYYY|/, 'At least we got a response.' );
393
394     $sip_user->delete;
395
396     dies_ok{ $msg->handle_sc_status( $server ) } ,"Dies if sip user cannot be found";
397
398 };
399
400 # Here is room for some more subtests
401
402 # END of main code
403
404 sub test_request_patron_status_v2 {
405     my $builder = t::lib::TestBuilder->new();
406     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
407     my ( $response, $findpatron );
408     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
409
410     my $patron1 = $builder->build({
411         source => 'Borrower',
412         value  => {
413             password => hash_password( PATRON_PW ),
414         },
415     });
416     my $card1 = $patron1->{cardnumber};
417     my $sip_patron1 = C4::SIP::ILS::Patron->new( $card1 );
418     $findpatron = $sip_patron1;
419
420     my $siprequest = PATRON_STATUS_REQ. 'engYYYYMMDDZZZZHHMMSS'.
421         FID_INST_ID. $branchcode. '|'.
422         FID_PATRON_ID. $card1. '|'.
423         FID_PATRON_PWD. PATRON_PW. '|';
424     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
425
426     my $server = { ils => $mocks->{ils} };
427     undef $response;
428     $msg->handle_patron_status( $server );
429
430     isnt( $response, undef, 'At least we got a response.' );
431     my $respcode = substr( $response, 0, 2 );
432     is( $respcode, PATRON_STATUS_RESP, 'Response code fine' );
433
434     check_field( $respcode, $response, FID_INST_ID, $branchcode , 'Verified institution id' );
435     check_field( $respcode, $response, FID_PATRON_ID, $card1, 'Verified patron id' );
436     check_field( $respcode, $response, FID_PERSONAL_NAME, $patron1->{surname}, 'Verified patron name', 'contains' );
437     check_field( $respcode, $response, FID_VALID_PATRON, 'Y', 'Verified code BL' );
438     check_field( $respcode, $response, FID_VALID_PATRON_PWD, 'Y', 'Verified code CQ' );
439     check_field( $respcode, $response, FID_SCREEN_MSG, '.+', 'Verified non-empty screen message', 'regex' );
440
441     # Now, we pass a wrong password and verify CQ again
442     $siprequest = PATRON_STATUS_REQ. 'engYYYYMMDDZZZZHHMMSS'.
443         FID_INST_ID. $branchcode. '|'.
444         FID_PATRON_ID. $card1. '|'.
445         FID_PATRON_PWD. 'wrong_password'. '|';
446     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
447     undef $response;
448     $msg->handle_patron_status( $server );
449     $respcode = substr( $response, 0, 2 );
450     check_field( $respcode, $response, FID_VALID_PATRON_PWD, 'N', 'Verified code CQ for wrong pw' );
451
452     # Check empty password and verify CQ again
453     $siprequest = PATRON_STATUS_REQ. 'engYYYYMMDDZZZZHHMMSS'.
454         FID_INST_ID. $branchcode. '|'.
455         FID_PATRON_ID. $card1. '|'.
456         FID_PATRON_PWD. '|';
457     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
458     undef $response;
459     $msg->handle_patron_status( $server );
460     $respcode = substr( $response, 0, 2 );
461     check_field( $respcode, $response, FID_VALID_PATRON_PWD, 'N', 'code CQ should be N for empty AD' );
462
463     # Finally, we send a wrong card number and check AE, BL
464     # This is done by removing the new patron first
465     Koha::Patrons->search({ cardnumber => $card1 })->delete;
466     undef $findpatron;
467     $siprequest = PATRON_STATUS_REQ. 'engYYYYMMDDZZZZHHMMSS'.
468         FID_INST_ID. $branchcode. '|'.
469         FID_PATRON_ID. $card1. '|'.
470         FID_PATRON_PWD. PATRON_PW. '|';
471     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
472     undef $response;
473     $msg->handle_patron_status( $server );
474     $respcode = substr( $response, 0, 2 );
475     check_field( $respcode, $response, FID_VALID_PATRON, 'N', 'Verified code BL for wrong cardnumber' );
476     check_field( $respcode, $response, FID_PERSONAL_NAME, '', 'Name should be empty now' );
477     check_field( $respcode, $response, FID_SCREEN_MSG, '.+', 'But we have a screen msg', 'regex' );
478 }
479
480 sub test_request_patron_info_v2 {
481     my $builder = t::lib::TestBuilder->new();
482     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
483     my ( $response, $findpatron );
484     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
485
486     my $patron2 = $builder->build({
487         source => 'Borrower',
488         value  => {
489             password => hash_password( PATRON_PW ),
490         },
491     });
492     my $card = $patron2->{cardnumber};
493     my $sip_patron2 = C4::SIP::ILS::Patron->new( $card );
494     $findpatron = $sip_patron2;
495     my $siprequest = PATRON_INFO. 'engYYYYMMDDZZZZHHMMSS'.'Y         '.
496         FID_INST_ID. $branchcode. '|'.
497         FID_PATRON_ID. $card. '|'.
498         FID_PATRON_PWD. PATRON_PW. '|';
499     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
500
501     my $server = { ils => $mocks->{ils} };
502     undef $response;
503     $msg->handle_patron_info( $server );
504     isnt( $response, undef, 'At least we got a response.' );
505     my $respcode = substr( $response, 0, 2 );
506     is( $respcode, PATRON_INFO_RESP, 'Response code fine' );
507
508     check_field( $respcode, $response, FID_INST_ID, $branchcode , 'Verified institution id' );
509     check_field( $respcode, $response, FID_PATRON_ID, $card, 'Verified patron id' );
510     check_field( $respcode, $response, FID_PERSONAL_NAME, $patron2->{surname}, 'Verified patron name', 'contains' );
511     check_field( $respcode, $response, FID_VALID_PATRON, 'Y', 'Verified code BL' );
512     check_field( $respcode, $response, FID_VALID_PATRON_PWD, 'Y', 'Verified code CQ' );
513     check_field( $respcode, $response, FID_FEE_LMT, '.*', 'Checked existence of fee limit', 'regex' );
514     check_field( $respcode, $response, FID_HOME_ADDR, $patron2->{address}, 'Address in BD', 'contains' );
515     check_field( $respcode, $response, FID_EMAIL, $patron2->{email}, 'Verified email in BE' );
516     check_field( $respcode, $response, FID_HOME_PHONE, $patron2->{phone}, 'Verified home phone in BF' );
517     # No check for custom fields here (unofficial PB, PC and PI)
518     check_field( $respcode, $response, FID_SCREEN_MSG, '.+', 'We have a screen msg', 'regex' );
519
520     # Test customized patron name in AE with same sip request
521     # This implicitly tests C4::SIP::ILS::Patron->name
522     $server->{account}->{ae_field_template} = "X[% patron.surname %]Y";
523     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
524     undef $response;
525     $msg->handle_patron_info( $server );
526     $respcode = substr( $response, 0, 2 );
527     check_field( $respcode, $response, FID_PERSONAL_NAME, 'X' . $patron2->{surname} . 'Y', 'Check customized patron name' );
528
529     undef $response;
530     $server->{account}->{hide_fields} = "BD,BE,BF,PB";
531     $msg->handle_patron_info( $server );
532     $respcode = substr( $response, 0, 2 );
533     check_field( $respcode, $response, FID_HOME_ADDR, undef, 'Home address successfully stripped from response' );
534     check_field( $respcode, $response, FID_EMAIL, undef, 'Email address successfully stripped from response' );
535     check_field( $respcode, $response, FID_HOME_PHONE, undef, 'Home phone successfully stripped from response' );
536     check_field( $respcode, $response, FID_PATRON_BIRTHDATE, undef, 'Date of birth successfully stripped from response' );
537     $server->{account}->{hide_fields} = "";
538
539     # Check empty password and verify CQ again
540     $siprequest = PATRON_INFO. 'engYYYYMMDDZZZZHHMMSS'.'Y         '.
541         FID_INST_ID. $branchcode. '|'.
542         FID_PATRON_ID. $card. '|'.
543         FID_PATRON_PWD. '|';
544     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
545     undef $response;
546     $msg->handle_patron_info( $server );
547     $respcode = substr( $response, 0, 2 );
548     check_field( $respcode, $response, FID_VALID_PATRON_PWD, 'N', 'code CQ should be N for empty AD' );
549     # Test empty password is OK if account configured to allow
550     $server->{account}->{allow_empty_passwords} = 1;
551     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
552     undef $response;
553     $msg->handle_patron_info( $server );
554     $respcode = substr( $response, 0, 2 );
555     check_field( $respcode, $response, FID_VALID_PATRON_PWD, 'Y', 'code CQ should be Y if empty AD allowed' );
556
557     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', '1' );
558     my $patron = Koha::Patrons->find({ cardnumber => $card });
559     $patron->update({ login_attempts => 0 });
560     is( $patron->account_locked, 0, "Patron account not locked already" );
561     $msg->handle_patron_info( $server );
562     $patron = Koha::Patrons->find({ cardnumber => $card });
563     is( $patron->account_locked, 0, "Patron account is not locked by patron info messages with empty password" );
564
565     # Finally, we send a wrong card number
566     Koha::Patrons->search({ cardnumber => $card })->delete;
567     undef $findpatron;
568     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
569     undef $response;
570     $msg->handle_patron_info( $server );
571     $respcode = substr( $response, 0, 2 );
572     check_field( $respcode, $response, FID_VALID_PATRON, 'N', 'Verified code BL for wrong cardnumber' );
573     check_field( $respcode, $response, FID_PERSONAL_NAME, '', 'Name should be empty now' );
574     check_field( $respcode, $response, FID_SCREEN_MSG, '.+', 'But we have a screen msg', 'regex' );
575 }
576
577 sub test_checkout_v2 {
578     my $builder = t::lib::TestBuilder->new();
579     my $branchcode  = $builder->build({ source => 'Branch' })->{branchcode};
580     my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode};
581     my ( $response, $findpatron );
582     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
583
584     # create some data
585     my $patron1 = $builder->build({
586         source => 'Borrower',
587         value  => {
588             password => hash_password( PATRON_PW ),
589         },
590     });
591     my $card1 = $patron1->{cardnumber};
592     my $sip_patron1 = C4::SIP::ILS::Patron->new( $card1 );
593     $findpatron = $sip_patron1;
594     my $item_object = $builder->build_sample_item({
595         damaged => 0,
596         withdrawn => 0,
597         itemlost => 0,
598         restricted => 0,
599         homebranch => $branchcode,
600         holdingbranch => $branchcode,
601     });
602
603     my $mockILS = $mocks->{ils};
604     my $server = { ils => $mockILS, account => {} };
605     $mockILS->mock( 'institution', sub { $branchcode; } );
606     $mockILS->mock( 'supports', sub { return; } );
607     $mockILS->mock( 'checkout', sub {
608         shift;
609         return C4::SIP::ILS->checkout(@_);
610     });
611     my $today = dt_from_string;
612     t::lib::Mocks::mock_userenv({ branchcode => $branchcode, flags => 1 });
613     t::lib::Mocks::mock_preference( 'CheckPrevCheckout',  'hardyes' );
614
615     my $issue = Koha::Checkout->new({ branchcode => $branchcode, borrowernumber => $patron1->{borrowernumber}, itemnumber => $item_object->itemnumber })->store;
616     my $return = AddReturn($item_object->barcode, $branchcode);
617
618     my $siprequest = CHECKOUT . 'YN' . siprequestdate($today) .
619     siprequestdate( $today->clone->add( days => 1) ) .
620     FID_INST_ID . $branchcode . '|'.
621     FID_PATRON_ID . $sip_patron1->id . '|' .
622     FID_ITEM_ID . $item_object->barcode . '|' .
623     FID_TERMINAL_PWD . 'ignored' . '|';
624     undef $response;
625
626     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
627     $server->{account}->{prevcheckout_block_checkout} = 1;
628     $msg->handle_checkout( $server );
629     my $respcode = substr( $response, 0, 2 );
630     check_field( $respcode, $response, FID_SCREEN_MSG, 'This item was previously checked out by you', 'Check screen msg', 'equals' );
631
632     is( Koha::Checkouts->search({ itemnumber => $item_object->id })->count, 0, "Item was not checked out (prevcheckout_block_checkout enabled)");
633
634     $server->{account}->{prevcheckout_block_checkout} = 0;
635     $msg->handle_checkout( $server );
636     $respcode = substr( $response, 0, 2 );
637     is( Koha::Checkouts->search({ itemnumber => $item_object->id })->count, 1, "Item was checked out (prevcheckout_block_checkout disabled)");
638
639     $msg->handle_checkout( $server );
640     ok( $response =~ m/AH\d{8}    \d{6}/, "Found AH field as timestamp in response");
641     $server->{account}->{format_due_date} = 1;
642     t::lib::Mocks::mock_preference( 'dateFormat',  'sql' );
643     undef $response;
644     $msg->handle_checkout( $server );
645     ok( $response =~ m/AH\d{4}-\d{2}-\d{2}/, "Found AH field as SQL date in response");
646
647 }
648
649 sub test_checkin_v2 {
650     my $builder = t::lib::TestBuilder->new();
651     my $branchcode  = $builder->build({ source => 'Branch' })->{branchcode};
652     my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode};
653     my ( $response, $findpatron );
654     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
655
656     # create some data
657     my $patron1 = $builder->build({
658         source => 'Borrower',
659         value  => {
660             password => hash_password( PATRON_PW ),
661         },
662     });
663     my $card1 = $patron1->{cardnumber};
664     my $sip_patron1 = C4::SIP::ILS::Patron->new( $card1 );
665     $findpatron = $sip_patron1;
666     my $item_object = $builder->build_sample_item({
667         damaged => 0,
668         withdrawn => 0,
669         itemlost => 0,
670         restricted => 0,
671         homebranch => $branchcode,
672         holdingbranch => $branchcode,
673     });
674
675     my $mockILS = $mocks->{ils};
676     my $server = { ils => $mockILS, account => {} };
677     $mockILS->mock( 'institution', sub { $branchcode; } );
678     $mockILS->mock( 'supports', sub { return; } );
679     $mockILS->mock( 'checkin', sub {
680         shift;
681         return C4::SIP::ILS->checkin(@_);
682     });
683     my $today = dt_from_string;
684
685     # Checkin invalid barcode
686     Koha::Items->search({ barcode => 'not_to_be_found' })->delete;
687     my $siprequest = CHECKIN . 'N' . 'YYYYMMDDZZZZHHMMSS' .
688         siprequestdate( $today->clone->add( days => 1) ) .
689         FID_INST_ID . $branchcode . '|'.
690         FID_ITEM_ID . 'not_to_be_found' . '|' .
691         FID_TERMINAL_PWD . 'ignored' . '|';
692     undef $response;
693     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
694     warnings_like { $msg->handle_checkin( $server ); }
695         [ qr/No item 'not_to_be_found'/, qr/no item found in object to resensitize/ ],
696         'Checkin of invalid item with two warnings';
697     my $respcode = substr( $response, 0, 2 );
698     is( $respcode, CHECKIN_RESP, 'Response code fine' );
699     is( substr($response,2,1), '0', 'OK flag is false' );
700     is( substr($response,5,1), 'Y', 'Alert flag is set' );
701     check_field( $respcode, $response, FID_SCREEN_MSG, 'Invalid Item', 'Check screen msg', 'regex' );
702
703     # Not checked out, toggle option checked_in_ok
704     $siprequest = CHECKIN . 'N' . 'YYYYMMDDZZZZHHMMSS' .
705         siprequestdate( $today->clone->add( days => 1) ) .
706         FID_INST_ID . $branchcode . '|'.
707         FID_ITEM_ID . $item_object->barcode . '|' .
708         FID_TERMINAL_PWD . 'ignored' . '|';
709     undef $response;
710     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
711     $msg->handle_checkin( $server );
712     $respcode = substr( $response, 0, 2 );
713     is( substr($response,2,1), '0', 'OK flag is false when checking in an item that was not checked out' );
714     is( substr($response,5,1), 'Y', 'Alert flag is set' );
715     check_field( $respcode, $response, FID_SCREEN_MSG, 'not checked out', 'Check screen msg', 'regex' );
716     # Toggle option
717     $server->{account}->{checked_in_ok} = 1;
718     undef $response;
719     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
720     $msg->handle_checkin( $server );
721     is( substr($response,2,1), '1', 'OK flag is true now with checked_in_ok flag set when checking in an item that was not checked out' );
722     is( substr($response,5,1), 'N', 'Alert flag no longer set' );
723     check_field( $respcode, $response, FID_SCREEN_MSG, undef, 'No screen msg' );
724
725     # Move item to another holding branch to trigger CV of 04 with alert flag
726     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'holdingbranch' );
727     $item_object->holdingbranch( $branchcode2 )->store();
728     undef $response;
729     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
730     $msg->handle_checkin( $server );
731     is( substr($response,5,1), 'Y', 'Alert flag is set with check_in_ok, item is checked in but needs transfer' );
732     check_field( $respcode, $response, FID_ALERT_TYPE, '04', 'Got FID_ALERT_TYPE (CV) field with value 04 ( needs transfer )' );
733     $item_object->holdingbranch( $branchcode )->store();
734     t::lib::Mocks::mock_preference( ' AllowReturnToBranch ', 'anywhere' );
735
736     $server->{account}->{cv_send_00_on_success} = 0;
737     undef $response;
738     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
739     $msg->handle_checkin( $server );
740     $respcode = substr( $response, 0, 2 );
741     check_field( $respcode, $response, FID_ALERT_TYPE, undef, 'No FID_ALERT_TYPE (CV) field' );
742     $server->{account}->{cv_send_00_on_success} = 1;
743     undef $response;
744     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
745     $msg->handle_checkin( $server );
746     $respcode = substr( $response, 0, 2 );
747     check_field( $respcode, $response, FID_ALERT_TYPE, '00', 'FID_ALERT_TYPE (CV) field is 00' );
748     $server->{account}->{checked_in_ok} = 0;
749     $server->{account}->{cv_send_00_on_success} = 0;
750
751     t::lib::Mocks::mock_preference( 'RecordLocalUseOnReturn', '1' );
752     $server->{account}->{checked_in_ok} = 0;
753     $server->{account}->{cv_triggers_alert} = 0;
754     undef $response;
755     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
756     $msg->handle_checkin( $server );
757     $respcode = substr( $response, 0, 2 );
758     is( substr( $response, 5, 1 ), 'Y', 'Checkin without CV triggers alert flag when cv_triggers_alert is off' );
759     t::lib::Mocks::mock_preference( 'RecordLocalUseOnReturn', '0' );
760     $server->{account}->{cv_triggers_alert} = 1;
761     undef $response;
762     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
763     $msg->handle_checkin( $server );
764     $respcode = substr( $response, 0, 2 );
765     is( substr( $response, 5, 1 ), 'N', 'Checkin without CV does not trigger alert flag when cv_triggers_alert is on' );
766     $server->{account}->{cv_triggers_alert} = 0;
767     t::lib::Mocks::mock_preference( 'RecordLocalUseOnReturn', '1' );
768
769     $server->{account}->{checked_in_ok} = 1;
770     $server->{account}->{ct_always_send} = 0;
771     undef $response;
772     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
773     $msg->handle_checkin( $server );
774     $respcode = substr( $response, 0, 2 );
775     check_field( $respcode, $response, FID_DESTINATION_LOCATION, undef, 'No FID_DESTINATION_LOCATION (CT) field' );
776     $server->{account}->{ct_always_send} = 1;
777     undef $response;
778     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
779     $msg->handle_checkin( $server );
780     $respcode = substr( $response, 0, 2 );
781     check_field( $respcode, $response, FID_DESTINATION_LOCATION, q{}, 'FID_DESTINATION_LOCATION (CT) field is empty but present' );
782     $server->{account}->{checked_in_ok} = 0;
783     $server->{account}->{ct_always_send} = 0;
784
785     # Checkin at wrong branch: issue item and switch branch, and checkin
786     my $issue = Koha::Checkout->new({ branchcode => $branchcode, borrowernumber => $patron1->{borrowernumber}, itemnumber => $item_object->itemnumber })->store;
787     $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
788     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'homebranch' );
789     undef $response;
790     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
791     $msg->handle_checkin( $server );
792     is( substr($response,2,1), '0', 'OK flag is false when we check in at the wrong branch and we do not allow it' );
793     is( substr($response,5,1), 'Y', 'Alert flag is set' );
794     check_field( $respcode, $response, FID_SCREEN_MSG, 'Checkin failed', 'Check screen msg' );
795     $branchcode = $item_object->homebranch;  # switch back
796     t::lib::Mocks::mock_preference( 'AllowReturnToBranch', 'anywhere' );
797
798     # Data corrupted: add same issue_id to old_issues
799     Koha::Old::Checkout->new({ issue_id => $issue->issue_id })->store;
800     undef $response;
801     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
802     warnings_like { $msg->handle_checkin( $server ); }
803         [ qr/Duplicate entry/, qr/data issues/ ],
804         'DBIx error on duplicate issue_id';
805     is( substr($response,2,1), '0', 'OK flag is false when we encounter data corruption in old_issues' );
806     is( substr($response,5,1), 'Y', 'Alert flag is set' );
807     check_field( $respcode, $response, FID_SCREEN_MSG, 'Checkin failed: data problem', 'Check screen msg' );
808
809     # Finally checkin without problems (remove duplicate id)
810     Koha::Old::Checkouts->search({ issue_id => $issue->issue_id })->delete;
811     undef $response;
812     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
813     $msg->handle_checkin( $server );
814     is( substr($response,2,1), '1', 'OK flag is true when we checkin after removing the duplicate' );
815     is( substr($response,5,1), 'N', 'Alert flag is not set' );
816     is( Koha::Checkouts->find( $issue->issue_id ), undef,
817         'Issue record is gone now' );
818
819     # Test account option no_holds_check that prevents items on hold from being checked in via SIP
820     $issue = Koha::Checkout->new({ branchcode => $branchcode, borrowernumber => $patron1->{borrowernumber}, itemnumber => $item_object->itemnumber })->store;
821     is( Koha::Checkouts->search({ itemnumber => $item_object->id })->count, 1, "Item is checked out");
822     Koha::Old::Checkouts->search({ issue_id => $issue->issue_id })->delete;
823     $server->{account}->{holds_block_checkin} = 1;
824     my $reserve_id = AddReserve({
825         branchcode     => $branchcode,
826         borrowernumber => $patron1->{borrowernumber},
827         biblionumber   => $item_object->biblionumber,
828         priority       => 1,
829     });
830     my $hold = Koha::Holds->find( $reserve_id );
831     is( $hold->id, $reserve_id, "Hold was created successfully" );
832     undef $response;
833     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
834     $msg->handle_checkin( $server );
835     is( substr($response,2,1), '0', 'OK flag is false when we check in an item on hold and we do not allow it' );
836     is( substr($response,5,1), 'Y', 'Alert flag is set' );
837     is( Koha::Checkouts->search({ itemnumber => $item_object->id })->count, 1, "Item was not checked in");
838     $hold->discard_changes;
839     is( $hold->found, undef, "Hold was not marked as found by SIP when holds_block_checkin enabled");
840     $server->{account}->{holds_block_checkin} = 0;
841
842     # Test account option holds_get_captured that automatically sets the hold as found for a hold and possibly sets it to in transit
843     $server->{account}->{holds_get_captured} = 0;
844     undef $response;
845     $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
846     $msg->handle_checkin( $server );
847     is( substr($response,2,1), '1', 'OK flag is true when we check in an item on hold and we allow it but do not capture it' );
848     is( substr($response,5,1), 'Y', 'Alert flag is set' );
849     is( Koha::Checkouts->search({ itemnumber => $item_object->id })->count, 0, "Item was checked in");
850     $hold->discard_changes;
851     is( $hold->found, undef, "Hold was not marked as found by SIP when holds_get_captured disabled");
852     $hold->delete();
853     $server->{account}->{holds_get_captured} = 1;
854 }
855
856 sub test_hold_patron_bcode {
857     my $builder = t::lib::TestBuilder->new();
858     my $branchcode  = $builder->build({ source => 'Branch' })->{branchcode};
859     my ( $response, $findpatron );
860     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
861
862     my $item = $builder->build_sample_item(
863         {
864             library => $branchcode
865         }
866     );
867
868     my $server = { ils => $mocks->{ils} };
869     my $sip_item = C4::SIP::ILS::Item->new( $item->barcode );
870
871     is( $sip_item->hold_patron_bcode, q{}, "SIP item with no hold returns empty string" );
872
873     my $resp = C4::SIP::Sip::maybe_add( FID_CALL_NUMBER, $sip_item->hold_patron_bcode, $server );
874     is( $resp, q{}, "maybe_add returns empty string for SIP item with no hold returns empty string" );
875 }
876
877 sub test_checkout_desensitize {
878     my $builder = t::lib::TestBuilder->new();
879     my $branchcode  = $builder->build({ source => 'Branch' })->{branchcode};
880     my ( $response, $findpatron );
881     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
882
883     # create some data
884     my $patron1 = $builder->build({
885         source => 'Borrower',
886         value  => {
887             password => hash_password( PATRON_PW ),
888         },
889     });
890     my $card1 = $patron1->{cardnumber};
891     my $sip_patron1 = C4::SIP::ILS::Patron->new( $card1 );
892     my $patron_category = $sip_patron1->ptype();
893     $findpatron = $sip_patron1;
894     my $item_object = $builder->build_sample_item({
895         damaged => 0,
896         withdrawn => 0,
897         itemlost => 0,
898         restricted => 0,
899         homebranch => $branchcode,
900         holdingbranch => $branchcode,
901     });
902
903     my $mockILS = $mocks->{ils};
904     my $server = { ils => $mockILS, account => {} };
905     $mockILS->mock( 'institution', sub { $branchcode; } );
906     $mockILS->mock( 'supports', sub { return; } );
907     $mockILS->mock( 'checkout', sub {
908         shift;
909         return C4::SIP::ILS->checkout(@_);
910     });
911     my $today = dt_from_string;
912     t::lib::Mocks::mock_userenv({ branchcode => $branchcode, flags => 1 });
913     t::lib::Mocks::mock_preference( 'CheckPrevCheckout',  'hardyes' );
914
915     my $siprequest = CHECKOUT . 'YN' . siprequestdate($today) .
916     siprequestdate( $today->clone->add( days => 1) ) .
917     FID_INST_ID . $branchcode . '|'.
918     FID_PATRON_ID . $sip_patron1->id . '|' .
919     FID_ITEM_ID . $item_object->barcode . '|' .
920     FID_TERMINAL_PWD . 'ignored' . '|';
921
922     undef $response;
923     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
924     $server->{account}->{inhouse_patron_categories} = "A,$patron_category,Z";
925     $msg->handle_checkout( $server );
926     my $respcode = substr( $response, 5, 1 );
927     is( $respcode, 'N', "Desensitize flag was not set for patron category in inhouse_patron_categories" );
928
929     undef $response;
930     $server->{account}->{inhouse_patron_categories} = "A,B,C";
931     $msg->handle_checkout( $server );
932     $respcode = substr( $response, 5, 1 );
933     is( $respcode, 'Y', "Desensitize flag was set for patron category not in inhouse_patron_categories" );
934
935     undef $response;
936     $server->{account}->{inhouse_patron_categories} = "";
937     $msg->handle_checkout( $server );
938     $respcode = substr( $response, 5, 1 );
939     is( $respcode, 'Y', "Desensitize flag was set for empty inhouse_patron_categories" );
940 }
941
942 sub test_renew_desensitize {
943     my $builder = t::lib::TestBuilder->new();
944     my $branchcode  = $builder->build({ source => 'Branch' })->{branchcode};
945     my ( $response, $findpatron );
946     my $mocks = create_mocks( \$response, \$findpatron, \$branchcode );
947
948     # create some data
949     my $patron1 = $builder->build({
950         source => 'Borrower',
951         value  => {
952             password => hash_password( PATRON_PW ),
953         },
954     });
955     my $card1 = $patron1->{cardnumber};
956     my $sip_patron1 = C4::SIP::ILS::Patron->new( $card1 );
957     my $patron_category = $sip_patron1->ptype();
958     $findpatron = $sip_patron1;
959     my $item_object = $builder->build_sample_item({
960         damaged => 0,
961         withdrawn => 0,
962         itemlost => 0,
963         restricted => 0,
964         homebranch => $branchcode,
965         holdingbranch => $branchcode,
966     });
967
968     my $mockILS = $mocks->{ils};
969     my $server = { ils => $mockILS, account => {} };
970     $mockILS->mock( 'institution', sub { $branchcode; } );
971     $mockILS->mock( 'supports', sub { return; } );
972     $mockILS->mock( 'checkout', sub {
973         shift;
974         return C4::SIP::ILS->checkout(@_);
975     });
976     my $today = dt_from_string;
977     t::lib::Mocks::mock_userenv({ branchcode => $branchcode, flags => 1 });
978
979     my $issue = Koha::Checkout->new({ branchcode => $branchcode, borrowernumber => $patron1->{borrowernumber}, itemnumber => $item_object->itemnumber })->store;
980
981     my $siprequest = RENEW . 'YN' . siprequestdate($today) .
982     siprequestdate( $today->clone->add( days => 1) ) .
983     FID_INST_ID . $branchcode . '|'.
984     FID_PATRON_ID . $sip_patron1->id . '|' .
985     FID_ITEM_ID . $item_object->barcode . '|' .
986     FID_TERMINAL_PWD . 'ignored' . '|';
987
988     undef $response;
989     my $msg = C4::SIP::Sip::MsgType->new( $siprequest, 0 );
990     $server->{account}->{inhouse_patron_categories} = "A,$patron_category,Z";
991     $msg->handle_checkout( $server );
992     my $respcode = substr( $response, 5, 1 );
993     is( $respcode, 'N', "Desensitize flag was not set for patron category in inhouse_patron_categories" );
994
995     undef $response;
996     $server->{account}->{inhouse_patron_categories} = "A,B,C";
997     $msg->handle_checkout( $server );
998     $respcode = substr( $response, 5, 1 );
999     is( $respcode, 'Y', "Desensitize flag was set for patron category not in inhouse_patron_categories" );
1000
1001     undef $response;
1002     $server->{account}->{inhouse_patron_categories} = "";
1003     $msg->handle_checkout( $server );
1004     $respcode = substr( $response, 5, 1 );
1005     is( $respcode, 'Y', "Desensitize flag was set for empty inhouse_patron_categories" );
1006 }
1007
1008 # Helper routines
1009
1010 sub create_mocks {
1011     my ( $response, $findpatron, $branchcode ) = @_; # referenced variables !
1012
1013     # mock write_msg (imported from Sip.pm into Message.pm)
1014     my $mockMsg = Test::MockModule->new( 'C4::SIP::Sip::MsgType' );
1015     $mockMsg->mock( 'write_msg', sub { $$response = $_[1]; } ); # save response
1016
1017     # mock ils object
1018     my $mockILS = Test::MockObject->new;
1019     $mockILS->mock( 'check_inst_id', sub {} );
1020     $mockILS->mock( 'institution_id', sub { $$branchcode; } );
1021     $mockILS->mock( 'find_patron', sub { $$findpatron; } );
1022
1023     return { ils => $mockILS, message => $mockMsg };
1024 }
1025
1026 sub check_field {
1027     my ( $code, $resp, $fld, $expr, $msg, $mode ) = @_;
1028     # mode: contains || equals || regex (by default: equals)
1029
1030     # strip fixed part; prefix to simplify next regex
1031     $resp = '|'. substr( $resp, fixed_length( $code ) );
1032     my $fldval;
1033     if( $resp =~ /\|$fld([^\|]*)\|/ ) {
1034         $fldval = $1;
1035     } elsif( !defined($expr) ) { # field should not be found
1036         ok( 1, $msg );
1037         return;
1038     } else { # test fails
1039         is( 0, 1, "Code $fld not found in '$resp'?" );
1040         return;
1041     }
1042
1043     if( !$mode || $mode eq 'equals' ) { # default
1044         is( $fldval, $expr, $msg );
1045     } elsif( $mode eq 'regex' ) {
1046         is( $fldval =~ /$expr/, 1, $msg );
1047     } else { # contains
1048         is( index( $fldval, $expr ) > -1, 1, $msg );
1049     }
1050 }
1051
1052 sub siprequestdate {
1053     my ( $dt ) = @_;
1054     return $dt->ymd('').(' 'x4).$dt->hms('');
1055 }
1056
1057 sub fixed_length { #length of fixed fields including response code
1058     return {
1059       ( PATRON_STATUS_RESP )  => 37,
1060       ( PATRON_INFO_RESP )    => 61,
1061       ( CHECKIN_RESP )        => 24,
1062       ( CHECKOUT_RESP )       => 24,
1063     }->{$_[0]};
1064 }