View | Details | Raw Unified | Return to bug 16330
Collapse All | Expand All

(-)a/Koha/Exceptions.pm (+9 lines)
Lines 8-13 use Exception::Class ( Link Here
8
    'Koha::Exceptions::Exception' => {
8
    'Koha::Exceptions::Exception' => {
9
        description => 'Something went wrong!',
9
        description => 'Something went wrong!',
10
    },
10
    },
11
    'Koha::Exceptions::BadParameter' => {
12
        isa => 'Koha::Exceptions::Exception',
13
        description => 'A bad parameter was given',
14
        fields => ['parameter'],
15
    },
11
    'Koha::Exceptions::DuplicateObject' => {
16
    'Koha::Exceptions::DuplicateObject' => {
12
        isa => 'Koha::Exceptions::Exception',
17
        isa => 'Koha::Exceptions::Exception',
13
        description => 'Same object already exists',
18
        description => 'Same object already exists',
Lines 24-29 use Exception::Class ( Link Here
24
        isa => 'Koha::Exceptions::Exception',
29
        isa => 'Koha::Exceptions::Exception',
25
        description => 'A required parameter is missing'
30
        description => 'A required parameter is missing'
26
    },
31
    },
32
    'Koha::Exceptions::NoChanges' => {
33
        isa => 'Koha::Exceptions::Exception',
34
        description => 'No changes were made',
35
    },
27
    'Koha::Exceptions::WrongParameter' => {
36
    'Koha::Exceptions::WrongParameter' => {
28
        isa => 'Koha::Exceptions::Exception',
37
        isa => 'Koha::Exceptions::Exception',
29
        description => 'One or more parameters are wrong',
38
        description => 'One or more parameters are wrong',
(-)a/Koha/Exceptions/Category.pm (+17 lines)
Line 0 Link Here
1
package Koha::Exceptions::Category;
2
3
use Modern::Perl;
4
5
use Exception::Class (
6
7
    'Koha::Exceptions::Category' => {
8
        description => 'Something went wrong!',
9
    },
10
    'Koha::Exceptions::Category::CategorycodeNotFound' => {
11
        isa => 'Koha::Exceptions::Category',
12
        description => "Category does not exist",
13
        fields => ["categorycode"],
14
    },
15
);
16
17
1;
(-)a/Koha/Exceptions/Library.pm (+17 lines)
Line 0 Link Here
1
package Koha::Exceptions::Library;
2
3
use Modern::Perl;
4
5
use Exception::Class (
6
7
    'Koha::Exceptions::Library' => {
8
        description => 'Something went wrong!',
9
    },
10
    'Koha::Exceptions::Library::BranchcodeNotFound' => {
11
        isa => 'Koha::Exceptions::Library',
12
        description => "Library does not exist",
13
        fields => ["branchcode"],
14
    },
15
);
16
17
1;
(-)a/Koha/Exceptions/Patron.pm (+17 lines)
Line 0 Link Here
1
package Koha::Exceptions::Patron;
2
3
use Modern::Perl;
4
5
use Exception::Class (
6
7
    'Koha::Exceptions::Patron' => {
8
        description => 'Something went wrong!',
9
    },
10
    'Koha::Exceptions::Patron::DuplicateObject' => {
11
        isa => 'Koha::Exceptions::Patron',
12
        description => "Patron cardnumber and userid must be unique",
13
        fields => ["conflict"],
14
    },
15
);
16
17
1;
(-)a/Koha/Patron.pm (+127 lines)
Lines 2-7 package Koha::Patron; Link Here
2
2
3
# Copyright ByWater Solutions 2014
3
# Copyright ByWater Solutions 2014
4
# Copyright PTFS Europe 2016
4
# Copyright PTFS Europe 2016
5
# Copyright Koha-Suomi Oy 2017
5
#
6
#
6
# This file is part of Koha.
7
# This file is part of Koha.
7
#
8
#
Lines 29-34 use Koha::Database; Link Here
29
use Koha::DateUtils;
30
use Koha::DateUtils;
30
use Koha::Holds;
31
use Koha::Holds;
31
use Koha::Old::Checkouts;
32
use Koha::Old::Checkouts;
33
use Koha::Exceptions;
34
use Koha::Exceptions::Category;
35
use Koha::Exceptions::Library;
36
use Koha::Exceptions::Patron;
37
use Koha::Libraries;
32
use Koha::Patron::Categories;
38
use Koha::Patron::Categories;
33
use Koha::Patron::HouseboundProfile;
39
use Koha::Patron::HouseboundProfile;
34
use Koha::Patron::HouseboundRole;
40
use Koha::Patron::HouseboundRole;
Lines 653-658 sub account_locked { Link Here
653
          and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
659
          and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
654
}
660
}
655
661
662
=head3 store
663
664
=cut
665
666
sub store {
667
    my ($self) = @_;
668
669
    # $self->_validate();
670
671
    return $self->SUPER::store();
672
}
673
656
=head3 type
674
=head3 type
657
675
658
=cut
676
=cut
Lines 661-666 sub _type { Link Here
661
    return 'Borrower';
679
    return 'Borrower';
662
}
680
}
663
681
682
=head2 Internal methods
683
684
=head3 _check_branchcode
685
686
Checks the existence of patron's branchcode and throws
687
Koha::Exceptions::Library::BranchcodeNotFound if branchcode is not found.
688
689
=cut
690
691
sub _check_branchcode {
692
    my ($self) = @_;
693
694
    return unless $self->branchcode;
695
    unless (Koha::Libraries->find($self->branchcode)) {
696
        Koha::Exceptions::Library::BranchcodeNotFound->throw(
697
            error => "Library does not exist",
698
            branchcode => $self->branchcode,
699
        );
700
    }
701
    return 1;
702
}
703
704
=head3 _check_categorycode
705
706
Checks the existence of patron's categorycode and throws
707
Koha::Exceptions::Category::CategorycodeNotFound if categorycode is not found.
708
709
=cut
710
711
sub _check_categorycode {
712
    my ($self) = @_;
713
714
    return unless $self->categorycode;
715
    unless (Koha::Patron::Categories->find($self->categorycode)) {
716
        Koha::Exceptions::Category::CategorycodeNotFound->throw(
717
            error => "Patron category does not exist",
718
            categorycode => $self->categorycode,
719
        );
720
    }
721
    return 1;
722
}
723
724
=head3 _check_uniqueness
725
726
Checks patron's cardnumber and userid for uniqueness and throws
727
Koha::Exceptions::Patron::DuplicateObject if conflicting with another patron.
728
729
=cut
730
731
sub _check_uniqueness {
732
    my ($self) = @_;
733
734
    my $select = {};
735
    $select->{cardnumber} = $self->cardnumber if $self->cardnumber;
736
    $select->{userid} = $self->userid if $self->userid;
737
738
    return unless keys %$select;
739
740
    # Find conflicting patrons
741
    my $patrons = Koha::Patrons->search({
742
        '-or' => $select
743
    });
744
745
    if ($patrons->count) {
746
        my $conflict = {};
747
        foreach my $patron ($patrons->as_list) {
748
            # New patron $self: a conflicting patron $patron found.
749
            # Updating patron $self: first make sure conflicting patron $patron is
750
            #                        not this patron $self.
751
            if (!$self->in_storage || $self->in_storage &&
752
            $self->borrowernumber != $patron->borrowernumber) {
753
                # Populate conflict information to exception
754
                if ($patron->cardnumber && $self->cardnumber &&
755
                    $patron->cardnumber eq $self->cardnumber)
756
                {
757
                    $conflict->{cardnumber} = $self->cardnumber;
758
                }
759
                if ($patron->userid && $self->userid &&
760
                    $patron->userid eq $self->userid)
761
                {
762
                    $conflict->{userid} = $self->userid;
763
                }
764
            }
765
        }
766
767
        Koha::Exceptions::Patron::DuplicateObject->throw(
768
            error => "Patron data conflicts with another patron",
769
            conflict => $conflict
770
        ) if keys %$conflict;
771
    }
772
    return 1;
773
}
774
775
=head3 _validate
776
777
Performs a set of validations on this object and throws Koha::Exceptions if errors
778
are found.
779
780
=cut
781
782
sub _validate {
783
    my ($self) = @_;
784
785
    $self->_check_branchcode;
786
    $self->_check_categorycode;
787
    $self->_check_uniqueness;
788
    return $self;
789
}
790
664
=head1 AUTHOR
791
=head1 AUTHOR
665
792
666
Kyle M Hall <kyle@bywatersolutions.com>
793
Kyle M Hall <kyle@bywatersolutions.com>
(-)a/Koha/REST/V1/Patron.pm (-5 / +163 lines)
Lines 19-41 use Modern::Perl; Link Here
19
19
20
use Mojo::Base 'Mojolicious::Controller';
20
use Mojo::Base 'Mojolicious::Controller';
21
21
22
use C4::Members qw( AddMember ModMember );
23
use Koha::AuthUtils qw(hash_password);
22
use Koha::Patrons;
24
use Koha::Patrons;
25
use Koha::Patron::Categories;
26
use Koha::Patron::Modifications;
27
use Koha::Libraries;
28
29
use Scalar::Util qw(blessed);
30
use Try::Tiny;
23
31
24
sub list {
32
sub list {
25
    my ($c, $args, $cb) = @_;
33
    my ($c, $args, $cb) = @_;
26
34
27
    my $user = $c->stash('koha.user');
35
    my $patrons;
36
    my $filter;
37
    $args //= {};
28
38
29
    my $patrons = Koha::Patrons->search;
39
    for my $filter_param ( keys %$args ) {
40
        $filter->{$filter_param} = { LIKE => $args->{$filter_param} . "%" };
41
    }
30
42
31
    $c->$cb($patrons, 200);
43
    return try {
44
        $patrons = Koha::Patrons->search($filter);
45
        return $c->$cb($patrons, 200);
46
    }
47
    catch {
48
        if ( $_->isa('DBIx::Class::Exception') ) {
49
            return $c->$cb( { error => $_->{msg} }, 500 );
50
        }
51
        else {
52
            return $c->$cb(
53
                { error => "Something went wrong, check the logs." }, 500 );
54
        }
55
    };
32
}
56
}
33
57
34
sub get {
58
sub get {
35
    my ($c, $args, $cb) = @_;
59
    my ($c, $args, $cb) = @_;
36
60
37
    my $user = $c->stash('koha.user');
38
39
    my $patron = Koha::Patrons->find($args->{borrowernumber});
61
    my $patron = Koha::Patrons->find($args->{borrowernumber});
40
    unless ($patron) {
62
    unless ($patron) {
41
        return $c->$cb({error => "Patron not found"}, 404);
63
        return $c->$cb({error => "Patron not found"}, 404);
Lines 44-47 sub get { Link Here
44
    return $c->$cb($patron, 200);
66
    return $c->$cb($patron, 200);
45
}
67
}
46
68
69
sub add {
70
    my ($c, $args, $cb) = @_;
71
72
    return try {
73
        my $body = $c->req->json;
74
75
        Koha::Patron->new($body)->_validate;
76
        # TODO: Use AddMember until it has been moved to Koha-namespace
77
        my $borrowernumber = AddMember(%$body);
78
        my $patron = Koha::Patrons->find($borrowernumber);
79
80
        return $c->$cb($patron, 201);
81
    }
82
    catch {
83
        unless (blessed $_ && $_->can('rethrow')) {
84
            return $c->$cb({ error =>
85
                "Something went wrong, check Koha logs for details."}, 500);
86
        }
87
        if ($_->isa('Koha::Exceptions::Patron::DuplicateObject')) {
88
            return $c->$cb({ error => $_->error, conflict => $_->conflict }, 409);
89
        }
90
        elsif ($_->isa('Koha::Exceptions::Library::BranchcodeNotFound')) {
91
            return $c->$cb({ error => "Given branchcode does not exist" }, 400);
92
        }
93
        elsif ($_->isa('Koha::Exceptions::Category::CategorycodeNotFound')) {
94
            return $c->$cb({ error => "Given categorycode does not exist"}, 400);
95
        }
96
        else {
97
            return $c->$cb({ error =>
98
                "Something went wrong, check Koha logs for details."}, 500);
99
        }
100
    };
101
}
102
103
sub edit {
104
    my ($c, $args, $cb) = @_;
105
106
    my $patron;
107
    return try {
108
        my $user = $c->stash('koha.user');
109
        $patron = Koha::Patrons->find($args->{borrowernumber});
110
        my $body = $c->req->json;
111
112
        $body->{borrowernumber} = $args->{borrowernumber};
113
114
        if (!C4::Auth::haspermission($user->userid, { borrowers => 1 }) &&
115
            $user->borrowernumber == $patron->borrowernumber){
116
            if (C4::Context->preference('OPACPatronDetails')) {
117
                $body = _delete_unmodifiable_parameters($body);
118
                die unless $patron->set($body)->_validate;
119
                my $m = Koha::Patron::Modification->new($body)->store();
120
                return $c->$cb({}, 202);
121
            } else {
122
                return $c->$cb({ error => "You need a permission to change"
123
                                ." Your personal details"}, 403);
124
            }
125
        }
126
        else {
127
            delete $body->{borrowernumber};
128
            die unless $patron->set($body)->_validate;
129
            # TODO: Use ModMember until it has been moved to Koha-namespace
130
            $body->{borrowernumber} = $args->{borrowernumber};
131
            die unless ModMember(%$body);
132
            return $c->$cb($patron, 200);
133
        }
134
    }
135
    catch {
136
        unless ($patron) {
137
            return $c->$cb({error => "Patron not found"}, 404);
138
        }
139
        unless (blessed $_ && $_->can('rethrow')) {
140
            return $c->$cb({ error =>
141
                "Something went wrong, check Koha logs for details."}, 500);
142
        }
143
        if ($_->isa('Koha::Exceptions::Patron::DuplicateObject')) {
144
            return $c->$cb({ error => $_->error, conflict => $_->conflict }, 409);
145
        }
146
        elsif ($_->isa('Koha::Exceptions::Library::BranchcodeNotFound')) {
147
            return $c->$cb({ error => "Given branchcode does not exist" }, 400);
148
        }
149
        elsif ($_->isa('Koha::Exceptions::Category::CategorycodeNotFound')) {
150
            return $c->$cb({ error => "Given categorycode does not exist"}, 400);
151
        }
152
        elsif ($_->isa('Koha::Exceptions::MissingParameter')) {
153
            return $c->$cb({error => "Missing mandatory parameter(s)",
154
                            parameters => $_->parameter }, 400);
155
        }
156
        elsif ($_->isa('Koha::Exceptions::BadParameter')) {
157
            return $c->$cb({error => "Invalid parameter(s)",
158
                            parameters => $_->parameter }, 400);
159
        }
160
        elsif ($_->isa('Koha::Exceptions::NoChanges')) {
161
            return $c->$cb({error => "No changes have been made"}, 204);
162
        }
163
        else {
164
            return $c->$cb({ error =>
165
                "Something went wrong, check Koha logs for details."}, 500);
166
        }
167
    };
168
}
169
170
sub delete {
171
    my ($c, $args, $cb) = @_;
172
173
    my $patron;
174
175
    return try {
176
        $patron = Koha::Patrons->find($args->{borrowernumber});
177
        # check if loans, reservations, debarrment, etc. before deletion!
178
        my $res = $patron->delete;
179
180
        return $c->$cb({}, 200);
181
    }
182
    catch {
183
        unless ($patron) {
184
            return $c->$cb({error => "Patron not found"}, 404);
185
        }
186
        else {
187
            return $c->$cb({ error =>
188
                "Something went wrong, check Koha logs for details."}, 500);
189
        }
190
    };
191
}
192
193
sub _delete_unmodifiable_parameters {
194
    my ($body) = @_;
195
196
    my %columns = map { $_ => 1 } Koha::Patron::Modifications->columns;
197
    foreach my $param (keys %$body) {
198
        unless (exists $columns{$param}) {
199
            delete $body->{$param};
200
        }
201
    }
202
    return $body;
203
}
204
47
1;
205
1;
(-)a/api/v1/swagger/definitions/patron.json (-2 / +22 lines)
Lines 121-126 Link Here
121
    },
121
    },
122
    "dateofbirth": {
122
    "dateofbirth": {
123
      "type": ["string", "null"],
123
      "type": ["string", "null"],
124
      "format": "date",
124
      "description": "patron's date of birth"
125
      "description": "patron's date of birth"
125
    },
126
    },
126
    "branchcode": {
127
    "branchcode": {
Lines 133-142 Link Here
133
    },
134
    },
134
    "dateenrolled": {
135
    "dateenrolled": {
135
      "type": ["string", "null"],
136
      "type": ["string", "null"],
137
      "format": "date",
136
      "description": "date the patron was added to Koha"
138
      "description": "date the patron was added to Koha"
137
    },
139
    },
138
    "dateexpiry": {
140
    "dateexpiry": {
139
      "type": ["string", "null"],
141
      "type": ["string", "null"],
142
      "format": "date",
140
      "description": "date the patron's card is set to expire"
143
      "description": "date the patron's card is set to expire"
141
    },
144
    },
142
    "gonenoaddress": {
145
    "gonenoaddress": {
Lines 149-154 Link Here
149
    },
152
    },
150
    "debarred": {
153
    "debarred": {
151
      "type": ["string", "null"],
154
      "type": ["string", "null"],
155
      "format": "date",
152
      "description": "until this date the patron can only check-in"
156
      "description": "until this date the patron can only check-in"
153
    },
157
    },
154
    "debarredcomment": {
158
    "debarredcomment": {
Lines 268-279 Link Here
268
      "description": "produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'"
272
      "description": "produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'"
269
    },
273
    },
270
    "updated_on": {
274
    "updated_on": {
271
      "type": ["string", "null"],
275
      "type": "string",
276
      "format": "date-time",
272
      "description": "time of last change could be useful for synchronization with external systems (among others)"
277
      "description": "time of last change could be useful for synchronization with external systems (among others)"
273
    },
278
    },
274
    "lastseen": {
279
    "lastseen": {
275
      "type": ["string", "null"],
280
      "type": ["string", "null"],
281
      "format": "date-time",
276
      "description": "last time a patron has been seen (connected at the OPAC or staff interface)"
282
      "description": "last time a patron has been seen (connected at the OPAC or staff interface)"
283
    },
284
    "lang": {
285
      "type": "string",
286
      "description": "lang to use to send notices to this patron"
287
    },
288
    "login_attempts": {
289
      "type": "integer",
290
      "description": "number of failed login attemps"
291
    },
292
    "overdrive_auth_token": {
293
      "type": ["string", "null"],
294
      "description": "persist OverDrive auth token"
277
    }
295
    }
278
  }
296
  },
297
  "additionalProperties": false,
298
  "required": ["surname", "address", "city", "branchcode", "categorycode"]
279
}
299
}
(-)a/api/v1/swagger/paths/patrons.json (-4 / +728 lines)
Lines 6-11 Link Here
6
      "produces": [
6
      "produces": [
7
          "application/json"
7
          "application/json"
8
      ],
8
      ],
9
      "parameters": [{
10
        "name": "borrowernumber",
11
        "in": "query",
12
        "description": "Case insensetive 'starts_with' search on borrowernumber",
13
        "required": false,
14
        "type": "string"
15
      },
16
      {
17
        "name": "cardnumber",
18
        "in": "query",
19
        "description": "Case insensetive 'starts_with' search on cardnumber",
20
        "required": false,
21
        "type": "string"
22
      },
23
      {
24
        "name": "surname",
25
        "in": "query",
26
        "description": "Case insensetive 'starts_with' search on surname",
27
        "required": false,
28
        "type": "string"
29
      },
30
      {
31
        "name": "firstname",
32
        "in": "query",
33
        "description": "Case insensetive 'starts_with' search on firstname",
34
        "required": false,
35
        "type": "string"
36
      },
37
      {
38
        "name": "title",
39
        "in": "query",
40
        "description": "Case insensetive 'starts_with' search on title",
41
        "required": false,
42
        "type": "string"
43
      },
44
      {
45
        "name": "othernames",
46
        "in": "query",
47
        "description": "Case insensetive 'starts_with' search on othernames",
48
        "required": false,
49
        "type": "string"
50
      },
51
      {
52
        "name": "initials",
53
        "in": "query",
54
        "description": "Case insensetive 'starts_with' search on initials",
55
        "required": false,
56
        "type": "string"
57
      },
58
      {
59
        "name": "streetnumber",
60
        "in": "query",
61
        "description": "Case insensetive 'starts_with' search on streetnumber",
62
        "required": false,
63
        "type": "string"
64
      },
65
      {
66
        "name": "streettype",
67
        "in": "query",
68
        "description": "Case insensetive 'starts_with' search on streettype",
69
        "required": false,
70
        "type": "string"
71
      },
72
      {
73
        "name": "address",
74
        "in": "query",
75
        "description": "Case insensetive 'starts_with' search on address",
76
        "required": false,
77
        "type": "string"
78
      },
79
      {
80
        "name": "address2",
81
        "in": "query",
82
        "description": "Case insensetive 'starts_with' search on address2",
83
        "required": false,
84
        "type": "string"
85
      },
86
      {
87
        "name": "city",
88
        "in": "query",
89
        "description": "Case insensetive 'starts_with' search on city",
90
        "required": false,
91
        "type": "string"
92
      },
93
      {
94
        "name": "state",
95
        "in": "query",
96
        "description": "Case insensetive 'starts_with' search on state",
97
        "required": false,
98
        "type": "string"
99
      },
100
      {
101
        "name": "zipcode",
102
        "in": "query",
103
        "description": "Case insensetive 'starts_with' search on zipcode",
104
        "required": false,
105
        "type": "string"
106
      },
107
      {
108
        "name": "country",
109
        "in": "query",
110
        "description": "Case insensetive 'starts_with' search on country",
111
        "required": false,
112
        "type": "string"
113
      },
114
      {
115
        "name": "email",
116
        "in": "query",
117
        "description": "Case insensetive 'starts_with' search on email",
118
        "required": false,
119
        "type": "string"
120
      },
121
      {
122
        "name": "phone",
123
        "in": "query",
124
        "description": "Case insensetive 'starts_with' search on phone",
125
        "required": false,
126
        "type": "string"
127
      },
128
      {
129
        "name": "mobile",
130
        "in": "query",
131
        "description": "Case insensetive 'starts_with' search on mobile",
132
        "required": false,
133
        "type": "string"
134
      },
135
      {
136
        "name": "fax",
137
        "in": "query",
138
        "description": "Case insensetive 'starts_with' search on fax",
139
        "required": false,
140
        "type": "string"
141
      },
142
      {
143
        "name": "emailpro",
144
        "in": "query",
145
        "description": "Case insensetive 'starts_with' search on emailpro",
146
        "required": false,
147
        "type": "string"
148
      },
149
      {
150
        "name": "phonepro",
151
        "in": "query",
152
        "description": "Case insensetive 'starts_with' search on phonepro",
153
        "required": false,
154
        "type": "string"
155
      },
156
      {
157
        "name": "B_streetnumber",
158
        "in": "query",
159
        "description": "Case insensetive 'starts_with' search on B_streetnumber",
160
        "required": false,
161
        "type": "string"
162
      },
163
      {
164
        "name": "B_streettype",
165
        "in": "query",
166
        "description": "Case insensetive 'starts_with' search on B_streettype",
167
        "required": false,
168
        "type": "string"
169
      },
170
      {
171
        "name": "B_address",
172
        "in": "query",
173
        "description": "Case insensetive 'starts_with' search on B_address",
174
        "required": false,
175
        "type": "string"
176
      },
177
      {
178
        "name": "B_address2",
179
        "in": "query",
180
        "description": "Case insensetive 'starts_with' search on B_address2",
181
        "required": false,
182
        "type": "string"
183
      },
184
      {
185
        "name": "B_city",
186
        "in": "query",
187
        "description": "Case insensetive 'starts_with' search on B_city",
188
        "required": false,
189
        "type": "string"
190
      },
191
      {
192
        "name": "B_state",
193
        "in": "query",
194
        "description": "Case insensetive 'starts_with' search on B_state",
195
        "required": false,
196
        "type": "string"
197
      },
198
      {
199
        "name": "B_zipcode",
200
        "in": "query",
201
        "description": "Case insensetive 'starts_with' search on B_zipcode",
202
        "required": false,
203
        "type": "string"
204
      },
205
      {
206
        "name": "B_country",
207
        "in": "query",
208
        "description": "Case insensetive 'starts_with' search on B_country",
209
        "required": false,
210
        "type": "string"
211
      },
212
      {
213
        "name": "B_email",
214
        "in": "query",
215
        "description": "Case insensetive 'starts_with' search on B_email",
216
        "required": false,
217
        "type": "string"
218
      },
219
      {
220
        "name": "B_phone",
221
        "in": "query",
222
        "description": "Case insensetive 'starts_with' search on B_phone",
223
        "required": false,
224
        "type": "string"
225
      },
226
      {
227
        "name": "dateofbirth",
228
        "in": "query",
229
        "description": "Case insensetive 'starts_with' search on dateofbirth",
230
        "required": false,
231
        "type": "string"
232
      },
233
      {
234
        "name": "branchcode",
235
        "in": "query",
236
        "description": "Case insensetive 'starts_with' search on branchcode",
237
        "required": false,
238
        "type": "string"
239
      },
240
      {
241
        "name": "categorycode",
242
        "in": "query",
243
        "description": "Case insensetive 'starts_with' search on categorycode",
244
        "required": false,
245
        "type": "string"
246
      },
247
      {
248
        "name": "dateenrolled",
249
        "in": "query",
250
        "description": "Case insensetive 'starts_with' search on dateenrolled",
251
        "required": false,
252
        "type": "string"
253
      },
254
      {
255
        "name": "dateexpiry",
256
        "in": "query",
257
        "description": "Case insensetive 'starts_with' search on dateexpiry",
258
        "required": false,
259
        "type": "string"
260
      },
261
      {
262
        "name": "gonenoaddress",
263
        "in": "query",
264
        "description": "Case insensetive 'starts_with' search on gonenoaddress",
265
        "required": false,
266
        "type": "string"
267
      },
268
      {
269
        "name": "lost",
270
        "in": "query",
271
        "description": "Case insensetive 'starts_with' search on lost",
272
        "required": false,
273
        "type": "string"
274
      },
275
      {
276
        "name": "debarred",
277
        "in": "query",
278
        "description": "Case insensetive 'starts_with' search on debarred",
279
        "required": false,
280
        "type": "string"
281
      },
282
      {
283
        "name": "debarredcomment",
284
        "in": "query",
285
        "description": "Case insensetive 'starts_with' search on debarredcomment",
286
        "required": false,
287
        "type": "string"
288
      },
289
      {
290
        "name": "contactname",
291
        "in": "query",
292
        "description": "Case insensetive 'starts_with' search on contactname",
293
        "required": false,
294
        "type": "string"
295
      },
296
      {
297
        "name": "contactfirstname",
298
        "in": "query",
299
        "description": "Case insensetive 'starts_with' search on contactfirstname",
300
        "required": false,
301
        "type": "string"
302
      },
303
      {
304
        "name": "contacttitle",
305
        "in": "query",
306
        "description": "Case insensetive 'starts_with' search on contacttitle",
307
        "required": false,
308
        "type": "string"
309
      },
310
      {
311
        "name": "guarantorid",
312
        "in": "query",
313
        "description": "Case insensetive 'starts_with' search on guarantorid",
314
        "required": false,
315
        "type": "string"
316
      },
317
      {
318
        "name": "borrowernotes",
319
        "in": "query",
320
        "description": "Case insensetive 'starts_with' search on borrowernotes",
321
        "required": false,
322
        "type": "string"
323
      },
324
      {
325
        "name": "relationship",
326
        "in": "query",
327
        "description": "Case insensetive 'starts_with' search on relationship",
328
        "required": false,
329
        "type": "string"
330
      },
331
      {
332
        "name": "sex",
333
        "in": "query",
334
        "description": "Case insensetive 'starts_with' search on sex",
335
        "required": false,
336
        "type": "string"
337
      },
338
      {
339
        "name": "password",
340
        "in": "query",
341
        "description": "Case insensetive 'starts_with' search on password",
342
        "required": false,
343
        "type": "string"
344
      },
345
      {
346
        "name": "flags",
347
        "in": "query",
348
        "description": "Case insensetive 'starts_with' search on flags",
349
        "required": false,
350
        "type": "string"
351
      },
352
      {
353
        "name": "userid",
354
        "in": "query",
355
        "description": "Case insensetive 'starts_with' search on userid",
356
        "required": false,
357
        "type": "string"
358
      },
359
      {
360
        "name": "opacnote",
361
        "in": "query",
362
        "description": "Case insensetive 'starts_with' search on opacnote",
363
        "required": false,
364
        "type": "string"
365
      },
366
      {
367
        "name": "contactnote",
368
        "in": "query",
369
        "description": "Case insensetive 'starts_with' search on contactnote",
370
        "required": false,
371
        "type": "string"
372
      },
373
      {
374
        "name": "sort1",
375
        "in": "query",
376
        "description": "Case insensetive 'starts_with' search on sort1",
377
        "required": false,
378
        "type": "string"
379
      },
380
      {
381
        "name": "sort2",
382
        "in": "query",
383
        "description": "Case insensetive 'starts_with' search on sort2",
384
        "required": false,
385
        "type": "string"
386
      },
387
      {
388
        "name": "altcontactfirstname",
389
        "in": "query",
390
        "description": "Case insensetive 'starts_with' search on altcontactfirstname",
391
        "required": false,
392
        "type": "string"
393
      },
394
      {
395
        "name": "altcontactsurname",
396
        "in": "query",
397
        "description": "Case insensetive 'starts_with' search on altcontactsurname",
398
        "required": false,
399
        "type": "string"
400
      },
401
      {
402
        "name": "altcontactaddress1",
403
        "in": "query",
404
        "description": "Case insensetive 'starts_with' search on altcontactaddress1",
405
        "required": false,
406
        "type": "string"
407
      },
408
      {
409
        "name": "altcontactaddress2",
410
        "in": "query",
411
        "description": "Case insensetive 'starts_with' search on altcontactaddress2",
412
        "required": false,
413
        "type": "string"
414
      },
415
      {
416
        "name": "altcontactaddress3",
417
        "in": "query",
418
        "description": "Case insensetive 'starts_with' search on altcontactaddress3",
419
        "required": false,
420
        "type": "string"
421
      },
422
      {
423
        "name": "altcontactstate",
424
        "in": "query",
425
        "description": "Case insensetive 'starts_with' search on altcontactstate",
426
        "required": false,
427
        "type": "string"
428
      },
429
      {
430
        "name": "altcontactzipcode",
431
        "in": "query",
432
        "description": "Case insensetive 'starts_with' search on altcontactzipcode",
433
        "required": false,
434
        "type": "string"
435
      },
436
      {
437
        "name": "altcontactcountry",
438
        "in": "query",
439
        "description": "Case insensetive 'starts_with' search on altcontactcountry",
440
        "required": false,
441
        "type": "string"
442
      },
443
      {
444
        "name": "altcontactphone",
445
        "in": "query",
446
        "description": "Case insensetive 'starts_with' search on altcontactphone",
447
        "required": false,
448
        "type": "string"
449
      },
450
      {
451
        "name": "smsalertnumber",
452
        "in": "query",
453
        "description": "Case insensetive 'starts_with' search on smsalertnumber",
454
        "required": false,
455
        "type": "string"
456
      },
457
      {
458
        "name": "sms_provider_id",
459
        "in": "query",
460
        "description": "Case insensetive 'starts_with' search on sms_provider_id",
461
        "required": false,
462
        "type": "string"
463
      },
464
      {
465
        "name": "privacy",
466
        "in": "query",
467
        "description": "Case insensetive 'starts_with' search on privacy",
468
        "required": false,
469
        "type": "string"
470
      },
471
      {
472
        "name": "privacy_guarantor_checkouts",
473
        "in": "query",
474
        "description": "Case insensetive 'starts_with' search on privacy_guarantor_checkouts",
475
        "required": false,
476
        "type": "string"
477
      },
478
      {
479
        "name": "checkprevcheckout",
480
        "in": "query",
481
        "description": "Case insensetive 'starts_with' search on checkprevcheckout",
482
        "required": false,
483
        "type": "string"
484
      },
485
      {
486
        "name": "updated_on",
487
        "in": "query",
488
        "description": "Case insensetive 'starts_with' search on updated_on",
489
        "required": false,
490
        "type": "string"
491
      },
492
      {
493
        "name": "lastseen",
494
        "in": "query",
495
        "description": "Case insensetive 'starts_with' search on lastseen",
496
        "required": false,
497
        "type": "string"
498
      },
499
      {
500
        "name": "lang",
501
        "in": "query",
502
        "description": "Case insensetive 'starts_with' search on lang",
503
        "required": false,
504
        "type": "string"
505
      },
506
      {
507
        "name": "login_attempts",
508
        "in": "query",
509
        "description": "Case insensetive 'starts_with' search on login_attempts",
510
        "required": false,
511
        "type": "string"
512
      },
513
      {
514
        "name": "overdrive_auth_token",
515
        "in": "query",
516
        "description": "Case insensetive 'starts_with' search on overdrive_auth_token",
517
        "required": false,
518
        "type": "string"
519
      }],
9
      "responses": {
520
      "responses": {
10
        "200": {
521
        "200": {
11
          "description": "A list of patrons",
522
          "description": "A list of patrons",
Lines 16-26 Link Here
16
            }
527
            }
17
          }
528
          }
18
        },
529
        },
530
        "401": {
531
          "description": "Authentication required",
532
          "schema": {
533
            "$ref": "../definitions.json#/error"
534
          }
535
        },
536
        "403": {
537
          "description": "Access forbidden",
538
          "schema": {
539
            "$ref": "../definitions.json#/error"
540
          }
541
        },
542
        "500": {
543
          "description": "Internal server error",
544
          "schema": {
545
            "$ref": "../definitions.json#/error"
546
          }
547
        }
548
      },
549
      "x-koha-authorization": {
550
        "permissions": {
551
          "borrowers": "1"
552
        }
553
      }
554
    },
555
    "post": {
556
      "operationId": "addPatron",
557
      "tags": ["patrons"],
558
      "parameters": [{
559
        "name": "body",
560
        "in": "body",
561
        "description": "A JSON object containing information about the new patron",
562
        "required": true,
563
        "schema": {
564
          "$ref": "../definitions.json#/patron"
565
        }
566
      }],
567
      "consumes": ["application/json"],
568
      "produces": ["application/json"],
569
      "responses": {
570
        "201": {
571
          "description": "A successfully created patron",
572
          "schema": {
573
            "items": {
574
              "$ref": "../definitions.json#/patron"
575
            }
576
          }
577
        },
578
        "400": {
579
          "description": "Bad parameter",
580
          "schema": {
581
            "$ref": "../definitions.json#/error"
582
          }
583
        },
584
        "401": {
585
          "description": "Authentication required",
586
          "schema": {
587
            "$ref": "../definitions.json#/error"
588
          }
589
        },
19
        "403": {
590
        "403": {
20
          "description": "Access forbidden",
591
          "description": "Access forbidden",
21
          "schema": {
592
          "schema": {
22
            "$ref": "../definitions.json#/error"
593
            "$ref": "../definitions.json#/error"
23
          }
594
          }
595
        },
596
        "404": {
597
          "description": "Resource not found",
598
          "schema": {
599
            "$ref": "../definitions.json#/error"
600
          }
601
        },
602
        "409": {
603
          "description": "Conflict in creating resource",
604
          "schema": {
605
            "$ref": "../definitions.json#/error"
606
          }
607
        },
608
        "500": {
609
          "description": "Internal server error",
610
          "schema": {
611
            "$ref": "../definitions.json#/error"
612
          }
24
        }
613
        }
25
      },
614
      },
26
      "x-koha-authorization": {
615
      "x-koha-authorization": {
Lines 35-45 Link Here
35
      "operationId": "getPatron",
624
      "operationId": "getPatron",
36
      "tags": ["patrons"],
625
      "tags": ["patrons"],
37
      "parameters": [{
626
      "parameters": [{
38
          "$ref": "../parameters.json#/borrowernumberPathParam"
627
        "$ref": "../parameters.json#/borrowernumberPathParam"
39
        }
628
      }],
40
      ],
41
      "produces": [
629
      "produces": [
42
          "application/json"
630
        "application/json"
43
      ],
631
      ],
44
      "responses": {
632
      "responses": {
45
        "200": {
633
        "200": {
Lines 48-53 Link Here
48
            "$ref": "../definitions.json#/patron"
636
            "$ref": "../definitions.json#/patron"
49
          }
637
          }
50
        },
638
        },
639
        "401": {
640
          "description": "Authentication required",
641
          "schema": {
642
            "$ref": "../definitions.json#/error"
643
          }
644
        },
51
        "403": {
645
        "403": {
52
          "description": "Access forbidden",
646
          "description": "Access forbidden",
53
          "schema": {
647
          "schema": {
Lines 59-64 Link Here
59
          "schema": {
653
          "schema": {
60
            "$ref": "../definitions.json#/error"
654
            "$ref": "../definitions.json#/error"
61
          }
655
          }
656
        },
657
        "500": {
658
          "description": "Internal server error",
659
          "schema": {
660
            "$ref": "../definitions.json#/error"
661
          }
62
        }
662
        }
63
      },
663
      },
64
      "x-koha-authorization": {
664
      "x-koha-authorization": {
Lines 68-73 Link Here
68
          "borrowers": "1"
668
          "borrowers": "1"
69
        }
669
        }
70
      }
670
      }
671
    },
672
    "put": {
673
      "operationId": "editPatron",
674
      "tags": ["patrons"],
675
      "parameters": [
676
        {
677
          "$ref": "../parameters.json#/borrowernumberPathParam"
678
        },
679
        {
680
          "name": "body",
681
          "in": "body",
682
          "description": "A JSON object containing new information about existing patron",
683
          "required": true,
684
          "schema": {
685
            "$ref": "../definitions.json#/patron"
686
          }
687
        }
688
      ],
689
      "consumes": ["application/json"],
690
      "produces": ["application/json"],
691
      "responses": {
692
        "200": {
693
          "description": "A successfully updated patron",
694
          "schema": {
695
            "items": {
696
              "$ref": "../definitions.json#/patron"
697
            }
698
          }
699
        },
700
        "202": {
701
          "description": "Accepted and waiting for librarian verification",
702
          "schema": {
703
            "type": "object"
704
          }
705
        },
706
        "204": {
707
          "description": "No Content",
708
          "schema": {
709
            "type": "object"
710
          }
711
        },
712
        "400": {
713
          "description": "Bad parameter",
714
          "schema": {
715
            "$ref": "../definitions.json#/error"
716
          }
717
        },
718
        "403": {
719
          "description": "Access forbidden",
720
          "schema": {
721
            "$ref": "../definitions.json#/error"
722
          }
723
        },
724
        "404": {
725
          "description": "Resource not found",
726
          "schema": {
727
            "$ref": "../definitions.json#/error"
728
          }
729
        },
730
        "409": {
731
          "description": "Conflict in updating resource",
732
          "schema": {
733
            "$ref": "../definitions.json#/error"
734
          }
735
        },
736
        "500": {
737
          "description": "Internal server error",
738
          "schema": {
739
            "$ref": "../definitions.json#/error"
740
          }
741
        }
742
      },
743
      "x-koha-authorization": {
744
        "allow-owner": true,
745
        "allow-guarantor": true,
746
        "permissions": {
747
          "borrowers": "1"
748
        }
749
      }
750
    },
751
    "delete": {
752
      "operationId": "deletePatron",
753
      "tags": ["patrons"],
754
      "parameters": [{
755
        "$ref": "../parameters.json#/borrowernumberPathParam"
756
      }],
757
      "produces": ["application/json"],
758
      "responses": {
759
        "200": {
760
          "description": "Patron deleted successfully",
761
          "schema": {
762
            "type": "object"
763
          }
764
        },
765
        "400": {
766
          "description": "Patron deletion failed",
767
          "schema": {
768
            "$ref": "../definitions.json#/error"
769
          }
770
        },
771
        "401": {
772
          "description": "Authentication required",
773
          "schema": {
774
            "$ref": "../definitions.json#/error"
775
          }
776
        },
777
        "403": {
778
          "description": "Access forbidden",
779
          "schema": {
780
            "$ref": "../definitions.json#/error"
781
          }
782
        },
783
        "404": {
784
          "description": "Patron not found",
785
          "schema": {
786
            "$ref": "../definitions.json#/error"
787
          }
788
        }
789
      },
790
      "x-koha-authorization": {
791
        "permissions": {
792
          "borrowers": "1"
793
        }
794
      }
71
    }
795
    }
72
  }
796
  }
73
}
797
}
(-)a/t/db_dependent/Koha/Patrons.t (-1 / +95 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 22;
22
use Test::More tests => 23;
23
use Test::Warn;
23
use Test::Warn;
24
use DateTime;
24
use DateTime;
25
25
Lines 906-909 is( Koha::Patrons->search->count, $nb_of_patrons + 1, 'Delete should have delete Link Here
906
906
907
$schema->storage->txn_rollback;
907
$schema->storage->txn_rollback;
908
908
909
subtest '_validate() tests' => sub {
910
    plan tests => 4;
911
912
    $schema->storage->txn_begin;
913
914
    Koha::Patrons->delete;
915
916
    my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
917
    my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
918
    my $patron = $builder->build({
919
        source => 'Borrower',
920
        value => {
921
            branchcode   => $branchcode,
922
            cardnumber   => 'conflict',
923
            categorycode => $categorycode,
924
        }
925
    });
926
927
    ok(Koha::Patron->new({
928
        surname      => 'Store test',
929
        branchcode   => $branchcode,
930
        categorycode => $categorycode
931
    })->_validate->store, 'Stored a patron');
932
933
    subtest '_check_categorycode' => sub {
934
        plan tests => 2;
935
936
        my $conflicting = $builder->build({
937
            source => 'Borrower',
938
            value => {
939
                branchcode   => $branchcode,
940
                categorycode => 'nonexistent',
941
            }
942
        });
943
        delete $conflicting->{borrowernumber};
944
945
        eval { Koha::Patron->new($conflicting)->_validate };
946
947
        isa_ok($@, "Koha::Exceptions::Category::CategorycodeNotFound");
948
        is($@->{categorycode}, $conflicting->{categorycode},
949
           'Exception describes non-existent categorycode');
950
    };
951
952
    subtest '_check_categorycode' => sub {
953
        plan tests => 2;
954
955
        my $conflicting = $builder->build({
956
            source => 'Borrower',
957
            value => {
958
                branchcode   => 'nonexistent',
959
                categorycode => $categorycode,
960
            }
961
        });
962
        delete $conflicting->{borrowernumber};
963
964
        eval { Koha::Patron->new($conflicting)->_validate };
965
966
        isa_ok($@, "Koha::Exceptions::Library::BranchcodeNotFound");
967
        is($@->{branchcode}, $conflicting->{branchcode},
968
           'Exception describes non-existent branchcode');
969
    };
970
971
    subtest '_check_uniqueness() tests' => sub {
972
        plan tests => 4;
973
974
        my $conflicting = $builder->build({
975
            source => 'Borrower',
976
            value => {
977
                branchcode   => $branchcode,
978
                categorycode => $categorycode,
979
            }
980
        });
981
        delete $conflicting->{borrowernumber};
982
        $conflicting->{cardnumber} = 'conflict';
983
        $conflicting->{userid} = $patron->{userid};
984
985
        eval { Koha::Patron->new($conflicting)->_validate };
986
987
        isa_ok($@, "Koha::Exceptions::Patron::DuplicateObject");
988
        is($@->{conflict}->{cardnumber}, $conflicting->{cardnumber},
989
           'Exception describes conflicting cardnumber');
990
        is($@->{conflict}->{userid}, $conflicting->{userid},
991
           'Exception describes conflicting userid');
992
993
        $conflicting->{cardnumber} = 'notconflicting';
994
        $conflicting->{userid}     = 'notconflicting';
995
996
        ok(Koha::Patron->new($conflicting)->_validate->store, 'After modifying'
997
           .' cardnumber and userid to not conflict with others, no exception.');
998
    };
999
1000
    $schema->storage->txn_rollback;
1001
};
1002
909
1;
1003
1;
(-)a/t/db_dependent/api/v1/patrons.t (-105 / +396 lines)
Lines 17-134 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 21;
20
use Test::More tests => 5;
21
use Test::Mojo;
21
use Test::Mojo;
22
use Test::Warn;
23
22
use t::lib::TestBuilder;
24
use t::lib::TestBuilder;
25
use t::lib::Mocks;
23
26
24
use C4::Auth;
27
use C4::Auth;
25
use C4::Context;
28
use Koha::Cities;
26
27
use Koha::Database;
29
use Koha::Database;
28
use Koha::Patron;
29
30
30
my $builder = t::lib::TestBuilder->new();
31
my $schema  = Koha::Database->new->schema;
32
my $builder = t::lib::TestBuilder->new;
31
33
32
my $dbh = C4::Context->dbh;
34
# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
33
$dbh->{AutoCommit} = 0;
35
# this affects the other REST api tests
34
$dbh->{RaiseError} = 1;
36
t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
35
37
36
$ENV{REMOTE_ADDR} = '127.0.0.1';
38
my $remote_address = '127.0.0.1';
37
my $t = Test::Mojo->new('Koha::REST::V1');
39
my $t              = Test::Mojo->new('Koha::REST::V1');
38
40
39
my $categorycode = $builder->build({ source => 'Category' })->{ categorycode };
41
subtest 'list() tests' => sub {
40
my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode };
42
    plan tests => 2;
41
my $guarantor = $builder->build({
43
42
    source => 'Borrower',
44
    $schema->storage->txn_begin;
43
    value => {
45
44
        branchcode   => $branchcode,
46
    unauthorized_access_tests('GET', undef, undef);
45
        categorycode => $categorycode,
47
46
        flags        => 0,
48
    subtest 'librarian access tests' => sub {
47
    }
49
        plan tests => 8;
48
});
50
49
my $borrower = $builder->build({
51
        my ($borrowernumber, $sessionid) = create_user_and_session({
50
    source => 'Borrower',
52
            authorized => 1 });
51
    value => {
53
        my $patron = Koha::Patrons->find($borrowernumber);
52
        branchcode   => $branchcode,
54
        Koha::Patrons->search({
53
        categorycode => $categorycode,
55
            borrowernumber => { '!=' => $borrowernumber},
54
        flags        => 0,
56
            cardnumber => { LIKE => $patron->cardnumber . "%" }
55
        lost         => 1,
57
        })->delete;
56
        guarantorid  => $guarantor->{borrowernumber},
58
        Koha::Patrons->search({
57
    }
59
            borrowernumber => { '!=' => $borrowernumber},
58
});
60
            address2 => { LIKE => $patron->address2 . "%" }
59
61
        })->delete;
60
$t->get_ok('/api/v1/patrons')
62
61
  ->status_is(401);
63
        my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
62
64
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
63
$t->get_ok("/api/v1/patrons/" . $borrower->{ borrowernumber })
65
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
64
  ->status_is(401);
66
        $t->request_ok($tx)
65
67
          ->status_is(200);
66
my $session = C4::Auth::get_session('');
68
67
$session->param('number', $borrower->{ borrowernumber });
69
        $tx = $t->ua->build_tx(GET => '/api/v1/patrons?cardnumber='.
68
$session->param('id', $borrower->{ userid });
70
                                  $patron->cardnumber);
69
$session->param('ip', '127.0.0.1');
71
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
70
$session->param('lasttime', time());
72
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
71
$session->flush;
73
        $t->request_ok($tx)
72
74
          ->status_is(200)
73
my $session2 = C4::Auth::get_session('');
75
          ->json_is('/0/cardnumber' => $patron->cardnumber);
74
$session2->param('number', $guarantor->{ borrowernumber });
76
75
$session2->param('id', $guarantor->{ userid });
77
        $tx = $t->ua->build_tx(GET => '/api/v1/patrons?address2='.
76
$session2->param('ip', '127.0.0.1');
78
                                  $patron->address2);
77
$session2->param('lasttime', time());
79
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
78
$session2->flush;
80
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
79
81
        $t->request_ok($tx)
80
my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
82
          ->status_is(200)
81
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
83
          ->json_is('/0/address2' => $patron->address2);
82
$t->request_ok($tx)
84
    };
83
  ->status_is(403);
85
84
86
    $schema->storage->txn_rollback;
85
$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . ($borrower->{ borrowernumber }-1));
87
};
86
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
88
87
$t->request_ok($tx)
89
subtest 'get() tests' => sub {
88
  ->status_is(403)
90
    plan tests => 3;
89
  ->json_is('/required_permissions', {"borrowers" => "1"});
91
90
92
    $schema->storage->txn_begin;
91
# User without permissions, but is the owner of the object
93
92
$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $borrower->{borrowernumber});
94
    unauthorized_access_tests('GET', -1, undef);
93
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
95
94
$t->request_ok($tx)
96
    subtest 'access own object tests' => sub {
95
  ->status_is(200);
97
        plan tests => 4;
96
98
97
# User without permissions, but is the guarantor of the owner of the object
99
        my ($patronid, $patronsessionid) = create_user_and_session({
98
$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $borrower->{borrowernumber});
100
            authorized => 0 });
99
$tx->req->cookies({name => 'CGISESSID', value => $session2->id});
101
100
$t->request_ok($tx)
102
        # Access patron's own data even though they have no borrowers flag
101
  ->status_is(200)
103
        my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$patronid");
102
  ->json_is('/guarantorid', $guarantor->{borrowernumber});
104
        $tx->req->cookies({name => 'CGISESSID', value => $patronsessionid});
103
105
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
104
my $loggedinuser = $builder->build({
106
        $t->request_ok($tx)
105
    source => 'Borrower',
107
          ->status_is(200);
106
    value => {
108
109
        my $guarantee = $builder->build({
110
            source => 'Borrower',
111
            value  => {
112
                guarantorid => $patronid,
113
            }
114
        });
115
116
        # Access guarantee's data even though guarantor has no borrowers flag
117
        my $guaranteenumber = $guarantee->{borrowernumber};
118
        $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$guaranteenumber");
119
        $tx->req->cookies({name => 'CGISESSID', value => $patronsessionid});
120
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
121
        $t->request_ok($tx)
122
          ->status_is(200);
123
    };
124
125
    subtest 'librarian access tests' => sub {
126
        plan tests => 5;
127
128
        my ($patron_id) = create_user_and_session({
129
            authorized => 0 });
130
        my $patron = Koha::Patrons->find($patron_id);
131
        my ($borrowernumber, $sessionid) = create_user_and_session({
132
            authorized => 1 });
133
        my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$patron_id");
134
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
135
        $t->request_ok($tx)
136
          ->status_is(200)
137
          ->json_is('/borrowernumber' => $patron_id)
138
          ->json_is('/surname' => $patron->surname)
139
          ->json_is('/lost' => Mojo::JSON->false );
140
    };
141
142
    $schema->storage->txn_rollback;
143
};
144
145
subtest 'add() tests' => sub {
146
    plan tests => 2;
147
148
    $schema->storage->txn_begin;
149
150
    my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
151
    my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
152
    my $newpatron = {
153
        address      => 'Street',
107
        branchcode   => $branchcode,
154
        branchcode   => $branchcode,
155
        cardnumber   => $branchcode.$categorycode,
108
        categorycode => $categorycode,
156
        categorycode => $categorycode,
109
        flags        => 16 # borrowers flag
157
        city         => 'Joenzoo',
110
    }
158
        surname      => "TestUser",
111
});
159
        userid       => $branchcode.$categorycode,
112
160
    };
113
$session = C4::Auth::get_session('');
161
114
$session->param('number', $loggedinuser->{ borrowernumber });
162
    unauthorized_access_tests('POST', undef, $newpatron);
115
$session->param('id', $loggedinuser->{ userid });
163
116
$session->param('ip', '127.0.0.1');
164
    subtest 'librarian access tests' => sub {
117
$session->param('lasttime', time());
165
        plan tests => 18;
118
$session->flush;
166
119
167
        my ($borrowernumber, $sessionid) = create_user_and_session({
120
$tx = $t->ua->build_tx(GET => '/api/v1/patrons');
168
            authorized => 1 });
121
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
169
122
$tx->req->env({REMOTE_ADDR => '127.0.0.1'});
170
        $newpatron->{branchcode} = "nonexistent"; # Test invalid branchcode
123
$t->request_ok($tx)
171
        my $tx = $t->ua->build_tx(POST => "/api/v1/patrons" =>json => $newpatron);
124
  ->status_is(200);
172
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
125
173
        $t->request_ok($tx)
126
$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $borrower->{ borrowernumber });
174
          ->status_is(400)
127
$tx->req->cookies({name => 'CGISESSID', value => $session->id});
175
          ->json_is('/error' => "Given branchcode does not exist");
128
$t->request_ok($tx)
176
        $newpatron->{branchcode} = $branchcode;
129
  ->status_is(200)
177
130
  ->json_is('/borrowernumber' => $borrower->{ borrowernumber })
178
        $newpatron->{categorycode} = "nonexistent"; # Test invalid patron category
131
  ->json_is('/surname' => $borrower->{ surname })
179
        $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
132
  ->json_is('/lost' => 1 );
180
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
133
181
        $t->request_ok($tx)
134
$dbh->rollback;
182
          ->status_is(400)
183
          ->json_is('/error' => "Given categorycode does not exist");
184
        $newpatron->{categorycode} = $categorycode;
185
186
        $newpatron->{falseproperty} = "Non existent property";
187
        $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
188
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
189
        $t->request_ok($tx)
190
          ->status_is(400);
191
        delete $newpatron->{falseproperty};
192
193
        $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
194
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
195
        $t->request_ok($tx)
196
          ->status_is(201, 'Patron created successfully')
197
          ->json_has('/borrowernumber', 'got a borrowernumber')
198
          ->json_is('/cardnumber', $newpatron->{ cardnumber })
199
          ->json_is('/surname' => $newpatron->{ surname })
200
          ->json_is('/firstname' => $newpatron->{ firstname });
201
        $newpatron->{borrowernumber} = $tx->res->json->{borrowernumber};
202
203
        $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
204
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
205
        $t->request_ok($tx)
206
          ->status_is(409)
207
          ->json_has('/error', 'Fails when trying to POST duplicate'.
208
                     ' cardnumber or userid')
209
          ->json_has('/conflict', {
210
                        userid => $newpatron->{ userid },
211
                        cardnumber => $newpatron->{ cardnumber }
212
                    }
213
            );
214
    };
215
216
    $schema->storage->txn_rollback;
217
};
218
219
subtest 'edit() tests' => sub {
220
    plan tests => 3;
221
222
    $schema->storage->txn_begin;
223
224
    unauthorized_access_tests('PUT', 123, {email => 'nobody@example.com'});
225
226
    subtest 'patron modifying own data' => sub {
227
        plan tests => 7;
228
229
        my ($borrowernumber, $sessionid) = create_user_and_session({
230
            authorized => 0 });
231
        my $patron = Koha::Patrons->find($borrowernumber)->TO_JSON;
232
233
        t::lib::Mocks::mock_preference("OPACPatronDetails", 0);
234
        my $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
235
                            $patron->{borrowernumber} => json => $patron);
236
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
237
        $t->request_ok($tx)
238
          ->status_is(403, 'OPACPatronDetails off - modifications not allowed.');
239
240
        t::lib::Mocks::mock_preference("OPACPatronDetails", 1);
241
        $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
242
                            $patron->{borrowernumber} => json => $patron);
243
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
244
        $t->request_ok($tx)
245
          ->status_is(204, 'Updating myself with my current data');
246
247
        $patron->{'firstname'} = "noob";
248
        $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
249
                            $patron->{borrowernumber} => json => $patron);
250
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
251
        $t->request_ok($tx)
252
          ->status_is(202, 'Updating myself with my current data');
253
254
        # Approve changes
255
        Koha::Patron::Modifications->find({
256
            borrowernumber => $patron->{borrowernumber},
257
            firstname => "noob"
258
        })->approve;
259
        is(Koha::Patrons->find({
260
            borrowernumber => $patron->{borrowernumber}})->firstname,
261
           "noob", "Changes approved");
262
    };
263
264
    subtest 'librarian access tests' => sub {
265
        plan tests => 20;
266
267
        t::lib::Mocks::mock_preference('minPasswordLength', 1);
268
        my ($borrowernumber, $sessionid) = create_user_and_session({
269
            authorized => 1 });
270
        my ($borrowernumber2, undef) = create_user_and_session({
271
            authorized => 0 });
272
        my $patron    = Koha::Patrons->find($borrowernumber2);
273
        my $newpatron = Koha::Patrons->find($borrowernumber2)->TO_JSON;
274
275
        my $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/-1" =>
276
                                  json => $newpatron);
277
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
278
        $t->request_ok($tx)
279
          ->status_is(404)
280
          ->json_has('/error', 'Fails when trying to PUT nonexistent patron');
281
282
        $newpatron->{categorycode} = 'nonexistent';
283
        $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
284
                    $newpatron->{borrowernumber} => json => $newpatron
285
        );
286
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
287
        $t->request_ok($tx)
288
          ->status_is(400)
289
          ->json_is('/error' => "Given categorycode does not exist");
290
        $newpatron->{categorycode} = $patron->categorycode;
291
292
        $newpatron->{branchcode} = 'nonexistent';
293
        $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
294
                    $newpatron->{borrowernumber} => json => $newpatron
295
        );
296
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
297
        $t->request_ok($tx)
298
          ->status_is(400)
299
          ->json_is('/error' => "Given branchcode does not exist");
300
        $newpatron->{branchcode} = $patron->branchcode;
301
302
        $newpatron->{falseproperty} = "Non existent property";
303
        $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
304
                    $newpatron->{borrowernumber} => json => $newpatron);
305
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
306
        $t->request_ok($tx)
307
          ->status_is(400)
308
          ->json_is('/errors/0/message' =>
309
                    'Properties not allowed: falseproperty.');
310
        delete $newpatron->{falseproperty};
311
312
        $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
313
                    $borrowernumber => json => $newpatron);
314
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
315
        $t->request_ok($tx)
316
          ->status_is(409)
317
          ->json_has('/error' => "Fails when trying to update to an existing"
318
                     ."cardnumber or userid")
319
          ->json_has('/conflict', {
320
                cardnumber => $newpatron->{ cardnumber },
321
                userid => $newpatron->{ userid }
322
                }
323
            );
324
325
        $newpatron->{ cardnumber } = $borrowernumber.$borrowernumber2;
326
        $newpatron->{ userid } = "user".$borrowernumber.$borrowernumber2;
327
        $newpatron->{ surname } = "user".$borrowernumber.$borrowernumber2;
328
329
        $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
330
                    $newpatron->{borrowernumber} => json => $newpatron);
331
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
332
        $t->request_ok($tx)
333
          ->status_is(200, 'Patron updated successfully')
334
          ->json_has($newpatron);
335
        is(Koha::Patrons->find($newpatron->{borrowernumber})->cardnumber,
336
           $newpatron->{ cardnumber }, 'Patron is really updated!');
337
    };
338
339
    $schema->storage->txn_rollback;
340
};
341
342
subtest 'delete() tests' => sub {
343
    plan tests => 2;
344
345
    $schema->storage->txn_begin;
346
347
    unauthorized_access_tests('DELETE', 123, undef);
348
349
    subtest 'librarian access test' => sub {
350
        plan tests => 4;
351
352
        my ($borrowernumber, $sessionid) = create_user_and_session({
353
            authorized => 1 });
354
        my ($borrowernumber2, $sessionid2) = create_user_and_session({
355
            authorized => 0 });
356
357
        my $tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/-1");
358
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
359
        $t->request_ok($tx)
360
          ->status_is(404, 'Patron not found');
361
362
        $tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/$borrowernumber2");
363
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
364
        $t->request_ok($tx)
365
          ->status_is(200, 'Patron deleted successfully');
366
    };
367
368
    $schema->storage->txn_rollback;
369
};
370
371
# Centralized tests for 401s and 403s assuming the endpoint requires
372
# borrowers flag for access
373
sub unauthorized_access_tests {
374
    my ($verb, $patronid, $json) = @_;
375
376
    my $endpoint = '/api/v1/patrons';
377
    $endpoint .= ($patronid) ? "/$patronid" : '';
378
379
    subtest 'unauthorized access tests' => sub {
380
        plan tests => 5;
381
382
        my $tx = $t->ua->build_tx($verb => $endpoint => json => $json);
383
        $t->request_ok($tx)
384
          ->status_is(401);
385
386
        my ($borrowernumber, $sessionid) = create_user_and_session({
387
            authorized => 0 });
388
389
        $tx = $t->ua->build_tx($verb => $endpoint => json => $json);
390
        $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
391
        $t->request_ok($tx)
392
          ->status_is(403)
393
          ->json_is('/required_permissions', {"borrowers" => "1"});
394
    };
395
}
396
397
sub create_user_and_session {
398
399
    my $args  = shift;
400
    my $flags = ( $args->{authorized} ) ? 16 : 0;
401
    my $dbh   = C4::Context->dbh;
402
403
    my $user = $builder->build(
404
        {
405
            source => 'Borrower',
406
            value  => {
407
                flags => $flags,
408
                gonenoaddress => 0,
409
                lost => 0,
410
                email => 'nobody@example.com',
411
                emailpro => 'nobody@example.com',
412
                B_email => 'nobody@example.com',
413
            }
414
        }
415
    );
416
417
    # Create a session for the authorized user
418
    my $session = C4::Auth::get_session('');
419
    $session->param( 'number',   $user->{borrowernumber} );
420
    $session->param( 'id',       $user->{userid} );
421
    $session->param( 'ip',       '127.0.0.1' );
422
    $session->param( 'lasttime', time() );
423
    $session->flush;
424
425
    return ( $user->{borrowernumber}, $session->id );
426
}
135
- 

Return to bug 16330