Bug 27857: Start a transaction foreach subtest
[koha.git] / t / db_dependent / Koha / Patron.t
1 #!/usr/bin/perl
2
3 # Copyright 2019 Koha Development team
4 #
5 # This file is part of Koha
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 7;
23 use Test::Exception;
24 use Test::Warn;
25
26 use Koha::Database;
27 use Koha::DateUtils qw(dt_from_string);
28 use Koha::Patrons;
29 use Koha::Patron::Relationships;
30
31 use t::lib::TestBuilder;
32 use t::lib::Mocks;
33
34 my $schema  = Koha::Database->new->schema;
35 my $builder = t::lib::TestBuilder->new;
36
37 subtest 'add_guarantor() tests' => sub {
38
39     plan tests => 6;
40
41     $schema->storage->txn_begin;
42
43     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'father1|father2' );
44
45     my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
46     my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
47
48     throws_ok
49         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber }); }
50         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
51         'Exception is thrown as no relationship passed';
52
53     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
54
55     throws_ok
56         { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father' }); }
57         'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
58         'Exception is thrown as a wrong relationship was passed';
59
60     is( $patron_1->guarantee_relationships->count, 0, 'No guarantors added' );
61
62     $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father1' });
63
64     my $guarantors = $patron_1->guarantor_relationships;
65
66     is( $guarantors->count, 1, 'No guarantors added' );
67
68     {
69         local *STDERR;
70         open STDERR, '>', '/dev/null';
71         throws_ok
72             { $patron_1->add_guarantor({ guarantor_id => $patron_2->borrowernumber, relationship => 'father2' }); }
73             'Koha::Exceptions::Patron::Relationship::DuplicateRelationship',
74             'Exception is thrown for duplicated relationship';
75         close STDERR;
76     }
77
78     $schema->storage->txn_rollback;
79 };
80
81 subtest 'relationships_debt() tests' => sub {
82
83     plan tests => 168;
84
85     $schema->storage->txn_begin;
86
87     t::lib::Mocks::mock_preference( 'borrowerRelationship', 'parent' );
88
89     my $parent_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 1" } });
90     my $parent_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Parent 2" } });
91     my $child_1 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Child 1" } });
92     my $child_2 = $builder->build_object({ class => 'Koha::Patrons', value => { firstname => "Child 2" } });
93
94     $child_1->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
95     $child_1->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
96     $child_2->add_guarantor({ guarantor_id => $parent_1->borrowernumber, relationship => 'parent' });
97     $child_2->add_guarantor({ guarantor_id => $parent_2->borrowernumber, relationship => 'parent' });
98
99     is( $child_1->guarantor_relationships->guarantors->count, 2, 'Child 1 has correct number of guarantors' );
100     is( $child_2->guarantor_relationships->guarantors->count, 2, 'Child 2 has correct number of guarantors' );
101     is( $parent_1->guarantee_relationships->guarantees->count, 2, 'Parent 1 has correct number of guarantees' );
102     is( $parent_2->guarantee_relationships->guarantees->count, 2, 'Parent 2 has correct number of guarantees' );
103
104     my $patrons = [ $parent_1, $parent_2, $child_1, $child_2 ];
105
106     # First test: No debt
107     my ($parent1_debt, $parent2_debt, $child1_debt, $child2_debt) = (0,0,0,0);
108     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
109
110     # Add debt to child_2
111     $child2_debt = 2;
112     $child_2->account->add_debit({ type => 'ACCOUNT', amount => $child2_debt, interface => 'commandline' });
113     is( $child_2->account->non_issues_charges, $child2_debt, 'Debt added to Child 2' );
114     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
115
116     $parent1_debt = 3;
117     $parent_1->account->add_debit({ type => 'ACCOUNT', amount => $parent1_debt, interface => 'commandline' });
118     is( $parent_1->account->non_issues_charges, $parent1_debt, 'Debt added to Parent 1' );
119     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
120
121     $parent2_debt = 5;
122     $parent_2->account->add_debit({ type => 'ACCOUNT', amount => $parent2_debt, interface => 'commandline' });
123     is( $parent_2->account->non_issues_charges, $parent2_debt, 'Parent 2 owes correct amount' );
124     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
125
126     $child1_debt = 7;
127     $child_1->account->add_debit({ type => 'ACCOUNT', amount => $child1_debt, interface => 'commandline' });
128     is( $child_1->account->non_issues_charges, $child1_debt, 'Child 1 owes correct amount' );
129     _test_combinations($patrons, $parent1_debt,$parent2_debt,$child1_debt,$child2_debt);
130
131     $schema->storage->txn_rollback;
132 };
133
134 sub _test_combinations {
135     my ( $patrons, $parent1_debt, $parent2_debt, $child1_debt, $child2_debt ) = @_;
136
137     # Options
138     # P1 => P1 + C1 + C2 ( - P1 ) ( + P2 )
139     # P2 => P2 + C1 + C2 ( - P2 ) ( + P1 )
140     # C1 => P1 + P2 + C1 + C2 ( - C1 )
141     # C2 => P1 + P2 + C1 + C2 ( - C2 )
142
143 # 3 params, count from 0 to 7 in binary ( 3 places ) to get the set of switches, then do that 4 times, one for each parent and child
144     for my $i ( 0 .. 7 ) {
145         my ( $only_this_guarantor, $include_guarantors, $include_this_patron )
146           = split '', sprintf( "%03b", $i );
147         for my $patron ( @$patrons ) {
148             if ( $only_this_guarantor
149                 && !$patron->guarantee_relationships->count )
150             {
151                 throws_ok {
152                     $patron->relationships_debt(
153                         {
154                             only_this_guarantor => $only_this_guarantor,
155                             include_guarantors  => $include_guarantors,
156                             include_this_patron => $include_this_patron
157                         }
158                     );
159                 }
160                 'Koha::Exceptions::BadParameter',
161                   'Exception is thrown as patron is not a guarantor';
162
163             }
164             else {
165
166                 my $debt = 0;
167                 if ( $patron->firstname eq 'Parent 1' ) {
168                     $debt += $parent1_debt if ($include_this_patron && $include_guarantors);
169                     $debt += $child1_debt + $child2_debt;
170                     $debt += $parent2_debt unless ($only_this_guarantor || !$include_guarantors);
171                 }
172                 elsif ( $patron->firstname eq 'Parent 2' ) {
173                     $debt += $parent2_debt if ($include_this_patron & $include_guarantors);
174                     $debt += $child1_debt + $child2_debt;
175                     $debt += $parent1_debt unless ($only_this_guarantor || !$include_guarantors);
176                 }
177                 elsif ( $patron->firstname eq 'Child 1' ) {
178                     $debt += $child1_debt if ($include_this_patron);
179                     $debt += $child2_debt;
180                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
181                 }
182                 else {
183                     $debt += $child2_debt if ($include_this_patron);
184                     $debt += $child1_debt;
185                     $debt += $parent1_debt + $parent2_debt if ($include_guarantors);
186                 }
187
188                 is(
189                     $patron->relationships_debt(
190                         {
191                             only_this_guarantor => $only_this_guarantor,
192                             include_guarantors  => $include_guarantors,
193                             include_this_patron => $include_this_patron
194                         }
195                     ),
196                     $debt,
197                     $patron->firstname
198                       . " debt of $debt calculated correctly for ( only_this_guarantor: $only_this_guarantor, include_guarantors: $include_guarantors, include_this_patron: $include_this_patron)"
199                 );
200             }
201         }
202     }
203 }
204
205 subtest 'add_enrolment_fee_if_needed() tests' => sub {
206
207     plan tests => 2;
208
209     subtest 'category has enrolment fee' => sub {
210         plan tests => 7;
211
212         $schema->storage->txn_begin;
213
214         my $category = $builder->build_object(
215             {
216                 class => 'Koha::Patron::Categories',
217                 value => {
218                     enrolmentfee => 20
219                 }
220             }
221         );
222
223         my $patron = $builder->build_object(
224             {
225                 class => 'Koha::Patrons',
226                 value => {
227                     categorycode => $category->categorycode
228                 }
229             }
230         );
231
232         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
233         is( $enrollment_fee * 1, 20, 'Enrolment fee amount is correct' );
234         my $account = $patron->account;
235         is( $patron->account->balance * 1, 20, 'Patron charged the enrolment fee' );
236         # second enrolment fee, new
237         $enrollment_fee = $patron->add_enrolment_fee_if_needed(0);
238         # third enrolment fee, renewal
239         $enrollment_fee = $patron->add_enrolment_fee_if_needed(1);
240         is( $patron->account->balance * 1, 60, 'Patron charged the enrolment fees' );
241
242         my @debits = $account->outstanding_debits;
243         is( scalar @debits, 3, '3 enrolment fees' );
244         is( $debits[0]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
245         is( $debits[1]->debit_type_code, 'ACCOUNT', 'Account type set correctly' );
246         is( $debits[2]->debit_type_code, 'ACCOUNT_RENEW', 'Account type set correctly' );
247
248         $schema->storage->txn_rollback;
249     };
250
251     subtest 'no enrolment fee' => sub {
252
253         plan tests => 3;
254
255         $schema->storage->txn_begin;
256
257         my $category = $builder->build_object(
258             {
259                 class => 'Koha::Patron::Categories',
260                 value => {
261                     enrolmentfee => 0
262                 }
263             }
264         );
265
266         my $patron = $builder->build_object(
267             {
268                 class => 'Koha::Patrons',
269                 value => {
270                     categorycode => $category->categorycode
271                 }
272             }
273         );
274
275         my $enrollment_fee = $patron->add_enrolment_fee_if_needed();
276         is( $enrollment_fee * 1, 0, 'No enrolment fee' );
277         my $account = $patron->account;
278         is( $patron->account->balance, 0, 'Patron not charged anything' );
279
280         my @debits = $account->outstanding_debits;
281         is( scalar @debits, 0, 'no debits' );
282
283         $schema->storage->txn_rollback;
284     };
285 };
286
287 subtest 'to_api() tests' => sub {
288
289     plan tests => 6;
290
291     $schema->storage->txn_begin;
292
293     my $patron_class = Test::MockModule->new('Koha::Patron');
294     $patron_class->mock(
295         'algo',
296         sub { return 'algo' }
297     );
298
299     my $patron = $builder->build_object(
300         {
301             class => 'Koha::Patrons',
302             value => {
303                 debarred => undef
304             }
305         }
306     );
307
308     my $restricted = $patron->to_api->{restricted};
309     ok( defined $restricted, 'restricted is defined' );
310     ok( !$restricted, 'debarred is undef, restricted evaluates to false' );
311
312     $patron->debarred( dt_from_string->add( days => 1 ) )->store->discard_changes;
313     $restricted = $patron->to_api->{restricted};
314     ok( defined $restricted, 'restricted is defined' );
315     ok( $restricted, 'debarred is defined, restricted evaluates to true' );
316
317     my $patron_json = $patron->to_api({ embed => { algo => {} } });
318     ok( exists $patron_json->{algo} );
319     is( $patron_json->{algo}, 'algo' );
320
321     $schema->storage->txn_rollback;
322 };
323
324 subtest 'login_attempts tests' => sub {
325     plan tests => 1;
326
327     $schema->storage->txn_begin;
328
329     my $patron = $builder->build_object(
330         {
331             class => 'Koha::Patrons',
332         }
333     );
334     my $patron_info = $patron->unblessed;
335     $patron->delete;
336     delete $patron_info->{login_attempts};
337     my $new_patron = Koha::Patron->new($patron_info)->store;
338     is( $new_patron->discard_changes->login_attempts, 0, "login_attempts defaults to 0 as expected");
339
340     $schema->storage->txn_rollback;
341 };
342
343 subtest 'is_superlibrarian() tests' => sub {
344
345     plan tests => 3;
346
347     $schema->storage->txn_begin;
348
349     my $patron = $builder->build_object(
350         {
351             class => 'Koha::Patrons',
352
353             value => {
354                 flags => 16
355             }
356         }
357     );
358
359     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
360
361     $patron->flags(1)->store->discard_changes;
362     is( $patron->is_superlibrarian, 1, 'Patron is a superlibrarian and the method returns the correct value' );
363
364     $patron->flags(0)->store->discard_changes;
365     is( $patron->is_superlibrarian, 0, 'Patron is not a superlibrarian and the method returns the correct value' );
366
367     $schema->storage->txn_rollback;
368 };
369
370 subtest 'extended_attributes' => sub {
371
372     plan tests => 15;
373
374     my $schema = Koha::Database->new->schema;
375     $schema->storage->txn_begin;
376
377     my $patron_1 = $builder->build_object({class=> 'Koha::Patrons'});
378     my $patron_2 = $builder->build_object({class=> 'Koha::Patrons'});
379
380     t::lib::Mocks::mock_userenv({ patron => $patron_1 });
381
382     my $attribute_type1 = Koha::Patron::Attribute::Type->new(
383         {
384             code        => 'my code1',
385             description => 'my description1',
386             unique_id   => 1
387         }
388     )->store;
389     my $attribute_type2 = Koha::Patron::Attribute::Type->new(
390         {
391             code             => 'my code2',
392             description      => 'my description2',
393             opac_display     => 1,
394             staff_searchable => 1
395         }
396     )->store;
397
398     my $attribute_type3 = $builder->build_object({ class => 'Koha::Patron::Attribute::Types' });
399
400     my $new_library = $builder->build( { source => 'Branch' } );
401     my $attribute_type_limited = Koha::Patron::Attribute::Type->new(
402         { code => 'my code3', description => 'my description3' } )->store;
403     $attribute_type_limited->library_limits( [ $new_library->{branchcode} ] );
404
405     my $attributes_for_1 = [
406         {
407             attribute => 'my attribute1',
408             code => $attribute_type1->code(),
409         },
410         {
411             attribute => 'my attribute2',
412             code => $attribute_type2->code(),
413         },
414         {
415             attribute => 'my attribute limited',
416             code => $attribute_type_limited->code(),
417         }
418     ];
419
420     my $attributes_for_2 = [
421         {
422             attribute => 'my attribute12',
423             code => $attribute_type1->code(),
424         },
425         {
426             attribute => 'my attribute limited 2',
427             code => $attribute_type_limited->code(),
428         }
429     ];
430
431     my $extended_attributes = $patron_1->extended_attributes;
432     is( ref($extended_attributes), 'Koha::Patron::Attributes', 'Koha::Patron->extended_attributes must return a Koha::Patron::Attribute set' );
433     is( $extended_attributes->count, 0, 'There should not be attribute yet');
434
435     $patron_1->extended_attributes->filter_by_branch_limitations->delete;
436     $patron_2->extended_attributes->filter_by_branch_limitations->delete;
437     $patron_1->extended_attributes($attributes_for_1);
438     $patron_2->extended_attributes($attributes_for_2);
439
440     my $extended_attributes_for_1 = $patron_1->extended_attributes;
441     is( $extended_attributes_for_1->count, 3, 'There should be 3 attributes now for patron 1');
442
443     my $extended_attributes_for_2 = $patron_2->extended_attributes;
444     is( $extended_attributes_for_2->count, 2, 'There should be 2 attributes now for patron 2');
445
446     my $attribute_12 = $extended_attributes_for_2->search({ code => $attribute_type1->code })->next;
447     is( $attribute_12->attribute, 'my attribute12', 'search by code should return the correct attribute' );
448
449     $attribute_12 = $patron_2->get_extended_attribute( $attribute_type1->code );
450     is( $attribute_12->attribute, 'my attribute12', 'Koha::Patron->get_extended_attribute should return the correct attribute value' );
451
452     my $expected_attributes_for_2 = [
453         {
454             code      => $attribute_type1->code(),
455             attribute => 'my attribute12',
456         },
457         {
458             code      => $attribute_type_limited->code(),
459             attribute => 'my attribute limited 2',
460         }
461     ];
462     # Sorting them by code
463     $expected_attributes_for_2 = [ sort { $a->{code} cmp $b->{code} } @$expected_attributes_for_2 ];
464     my @extended_attributes_for_2 = $extended_attributes_for_2->as_list;
465
466     is_deeply(
467         [
468             {
469                 code      => $extended_attributes_for_2[0]->code,
470                 attribute => $extended_attributes_for_2[0]->attribute
471             },
472             {
473                 code      => $extended_attributes_for_2[1]->code,
474                 attribute => $extended_attributes_for_2[1]->attribute
475             }
476         ],
477         $expected_attributes_for_2
478     );
479
480     # TODO - What about multiple? POD explains the problem
481     my $non_existent = $patron_2->get_extended_attribute( 'not_exist' );
482     is( $non_existent, undef, 'Koha::Patron->get_extended_attribute must return undef if the attribute does not exist' );
483
484     # Test branch limitations
485     t::lib::Mocks::mock_userenv({ patron => $patron_2 });
486     # Return all
487     $extended_attributes_for_1 = $patron_1->extended_attributes;
488     is( $extended_attributes_for_1->count, 3, 'There should be 2 attributes for patron 1, the limited one should be returned');
489
490     # Return filtered
491     $extended_attributes_for_1 = $patron_1->extended_attributes->filter_by_branch_limitations;
492     is( $extended_attributes_for_1->count, 2, 'There should be 2 attributes for patron 1, the limited one should be returned');
493
494     # Not filtered
495     my $limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
496     is( $limited_value->attribute, 'my attribute limited', );
497
498     ## Do we need a filtered?
499     #$limited_value = $patron_1->get_extended_attribute( $attribute_type_limited->code );
500     #is( $limited_value, undef, );
501
502     $schema->storage->txn_rollback;
503
504     subtest 'non-repeatable attributes tests' => sub {
505
506         plan tests => 3;
507
508         $schema->storage->txn_begin;
509
510         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
511         my $attribute_type = $builder->build_object(
512             {
513                 class => 'Koha::Patron::Attribute::Types',
514                 value => { repeatable => 0 }
515             }
516         );
517
518         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
519
520         throws_ok
521             {
522                 $patron->extended_attributes(
523                     [
524                         { code => $attribute_type->code, attribute => 'a' },
525                         { code => $attribute_type->code, attribute => 'b' }
526                     ]
527                 );
528             }
529             'Koha::Exceptions::Patron::Attribute::NonRepeatable',
530             'Exception thrown on non-repeatable attribute';
531
532         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
533
534         $schema->storage->txn_rollback;
535
536     };
537
538     subtest 'unique attributes tests' => sub {
539
540         plan tests => 5;
541
542         $schema->storage->txn_begin;
543
544         my $patron_1 = $builder->build_object({ class => 'Koha::Patrons' });
545         my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
546
547         my $attribute_type_1 = $builder->build_object(
548             {
549                 class => 'Koha::Patron::Attribute::Types',
550                 value => { unique => 1 }
551             }
552         );
553
554         my $attribute_type_2 = $builder->build_object(
555             {
556                 class => 'Koha::Patron::Attribute::Types',
557                 value => { unique => 0 }
558             }
559         );
560
561         is( $patron_1->extended_attributes->count, 0, 'patron_1 has no extended attributes' );
562         is( $patron_2->extended_attributes->count, 0, 'patron_2 has no extended attributes' );
563
564         $patron_1->extended_attributes(
565             [
566                 { code => $attribute_type_1->code, attribute => 'a' },
567                 { code => $attribute_type_2->code, attribute => 'a' }
568             ]
569         );
570
571         throws_ok
572             {
573                 $patron_2->extended_attributes(
574                     [
575                         { code => $attribute_type_1->code, attribute => 'a' },
576                         { code => $attribute_type_2->code, attribute => 'a' }
577                     ]
578                 );
579             }
580             'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint',
581             'Exception thrown on unique attribute';
582
583         is( $patron_1->extended_attributes->count, 2, 'Extended attributes stored' );
584         is( $patron_2->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
585
586         $schema->storage->txn_rollback;
587
588     };
589
590     subtest 'invalid type attributes tests' => sub {
591
592         plan tests => 3;
593
594         $schema->storage->txn_begin;
595
596         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
597
598         my $attribute_type_1 = $builder->build_object(
599             {
600                 class => 'Koha::Patron::Attribute::Types',
601                 value => { repeatable => 0 }
602             }
603         );
604
605         my $attribute_type_2 = $builder->build_object(
606             {
607                 class => 'Koha::Patron::Attribute::Types'
608             }
609         );
610
611         my $type_2 = $attribute_type_2->code;
612         $attribute_type_2->delete;
613
614         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
615
616         throws_ok
617             {
618                 $patron->extended_attributes(
619                     [
620                         { code => $attribute_type_1->code, attribute => 'a' },
621                         { code => $attribute_type_2->code, attribute => 'b' }
622                     ]
623                 );
624             }
625             'Koha::Exceptions::Patron::Attribute::InvalidType',
626             'Exception thrown on invalid attribute type';
627
628         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
629
630         $schema->storage->txn_rollback;
631
632     };
633
634     subtest 'globally mandatory attributes tests' => sub {
635
636         plan tests => 5;
637
638         $schema->storage->txn_begin;
639
640         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
641
642         my $attribute_type_1 = $builder->build_object(
643             {
644                 class => 'Koha::Patron::Attribute::Types',
645                 value => { mandatory => 1, class => 'a' }
646             }
647         );
648
649         my $attribute_type_2 = $builder->build_object(
650             {
651                 class => 'Koha::Patron::Attribute::Types',
652                 value => { mandatory => 0, class => 'a' }
653             }
654         );
655
656         is( $patron->extended_attributes->count, 0, 'Patron has no extended attributes' );
657
658         throws_ok
659             {
660                 $patron->extended_attributes(
661                     [
662                         { code => $attribute_type_2->code, attribute => 'b' }
663                     ]
664                 );
665             }
666             'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute',
667             'Exception thrown on missing mandatory attribute type';
668
669         is( $@->type, $attribute_type_1->code, 'Exception parameters are correct' );
670
671         is( $patron->extended_attributes->count, 0, 'Extended attributes storing rolled back' );
672
673         $patron->extended_attributes(
674             [
675                 { code => $attribute_type_1->code, attribute => 'b' }
676             ]
677         );
678
679         is( $patron->extended_attributes->count, 1, 'Extended attributes succeeded' );
680
681         $schema->storage->txn_rollback;
682
683     };
684
685 };