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

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

Return to bug 16330