diff --git a/.mailmap b/.mailmap index 9a650dd5da2..a27e49e9f5d 100644 --- a/.mailmap +++ b/.mailmap @@ -11,7 +11,7 @@ Aleisha Amohia Alex Arnaud Alex Arnaud = <=> -Alex Sassmannshausen A. Sassmannshausen +Alex Sassmannshausen A. Sassmannshausen Allen Reinmeyer Allen Reinmeyer Ambrose Li acli Amit Gupta amit gupta @@ -23,7 +23,7 @@ Antoine Farnault Arnaud Laurin Arnaud Laurin Artur Norrby -Aude Charillon +Aude Charillon Ava Li Azucena Aguayo Baptiste Bayche @@ -150,7 +150,7 @@ Ian Walls Indranil Das Gupta Indranil Das Gupta (L2C2 Technologies) Jake Deery -Jake Deery PerplexedTheta +Jake Deery PerplexedTheta Jake Deery Jane Wagner Janusz Kaczmarek @@ -231,7 +231,7 @@ Marcel de Rooy Marjorie Barry-Vila -Martin Renvoize +Martin Renvoize Mason James Mason James sushi Mason James szrj1m @@ -388,3 +388,19 @@ Vermont Organization of Koha Automated Libraries VOKAL Vermont Organization of Koha Automated Libraries Vermont Organization of Koha Automated Libraries [http://gmlc.org/index.php/vokal] Virginia Tech Virginia Tech [https://lib.vt.edu/] Waitaki Distict Council Waitaki Distict Council, NZ +Alex Sassmannshausen +Andrew Auld +Andrew Isherwood +Aude Charillon +Chloe Zermatten +Colin Campbell +David Roberts +Jacob O'Mara +Jake Deery +Janet McGowan +Jonathan Field +Mark Gavillet +Martin Renvoize +Matt Blenkinsop +Matt Blenkinsop +Pedro Amorim diff --git a/Koha/REST/V1/Lists.pm b/Koha/REST/V1/Lists.pm index a557627e7e1..38feb2273ac 100644 --- a/Koha/REST/V1/Lists.pm +++ b/Koha/REST/V1/Lists.pm @@ -16,17 +16,362 @@ package Koha::REST::V1::Lists; # along with Koha; if not, see . use Modern::Perl; - use Mojo::Base 'Mojolicious::Controller'; - use Koha::Virtualshelves; - use Try::Tiny qw( catch try ); +use Data::Dumper; + =head1 API =head2 Methods +=head3 add + +Create a virtual shelf + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + + # Check if owner_id exists if provided + my $body = $c->req->json; + if ( $body->{owner_id} ) { + my $owner = Koha::Patrons->find( $body->{owner_id} ); + unless ($owner) { + return $c->render( + status => 400, + openapi => { + error => "Invalid owner_id", + error_code => "invalid_owner" + } + ); + } + } + + # Set allow_change_from_staff=1 by default unless specified + $body->{allow_change_from_staff} = 1 unless exists $body->{allow_change_from_staff}; + + my $list = Koha::Virtualshelf->new_from_api($body); + $list->store->discard_changes; + + $c->res->headers->location( $c->req->url->to_string . '/' . $list->id ); + + return $c->render( + status => 201, + openapi => $list->to_api + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 update + +Update a virtual shelf + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $list = Koha::Virtualshelves->find( $c->param('list_id') ); + + return $c->render_resource_not_found("List") + unless $list; + + my $user = $c->stash('koha.user'); + + # Check if the list does not belong to the user + if ( $list->owner != $user->id ) { + + # Check if the user is allowed to modify the list + unless ( $list->allow_change_from_others || $list->allow_change_from_staff ) { + return $c->render( + status => 403, + openapi => { + error => "Cannot modify list without proper permissions", + error_code => "forbidden" + } + ); + } + } + + # Check allow_change_from_owner for own lists + if ( $list->owner == $user->id && $list->allow_change_from_owner == 0 ) { + return $c->render( + status => 403, + openapi => { + error => "Forbidden - list cannot be modified", + error_code => "forbidden" + } + ); + } + + return try { + $list->set_from_api( $c->req->json ); + $list->store(); + + return $c->render( + status => 200, + openapi => $list->to_api + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 delete + +Delete a virtual shelf if it exists + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $list = Koha::Virtualshelves->find( $c->param('list_id') ); + + return $c->render_resource_not_found("List") + unless $list; + + my $user = $c->stash('koha.user'); + + if ( $list->can_be_deleted( $user->id ) + || $list->allow_change_from_staff + || $list->allow_change_from_others + || ( $list->is_public && C4::Auth::haspermission( $user->userid, { lists => 'delete_public_lists' } ) ) ) + { + return try { + $list->delete; + return $c->render_resource_deleted; + } catch { + $c->unhandled_exception($_); + }; + } + + return $c->render( + status => 403, + openapi => { + error => "Forbidden - you are not allowed to delete this list", + error_code => "forbidden" + } + ); +} + +=head3 public_add + +Create a public virtual shelf + +=cut + +sub public_add { + my $c = shift->openapi->valid_input or return; + + my $user = $c->stash('koha.user'); + + # Handle anonymous users first + unless ($user) { + return $c->render( + status => 401, + openapi => { + error => "Authentication required", + error_code => "authentication_required" + } + ); + } + + my $body = $c->req->json; + $body->{owner} = $user->id; + + return try { + my $list = Koha::Virtualshelf->new_from_api($body); + $list->store->discard_changes; + + $c->res->headers->location( $c->req->url->to_string . '/' . $list->id ); + + return $c->render( + status => 201, + openapi => $list->to_api + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 public_get + +List the contents of a public virtual shelf or a virtual shelf you own + +=cut + +sub public_get { + my $c = shift->openapi->valid_input or return; + + return try { + my $list = Koha::Virtualshelves->find( $c->param('list_id') ); + + return $c->render_resource_not_found("List") + unless $list; + + my $user = $c->stash('koha.user'); + + # If no user is logged in, only allow access to public lists + unless ($user) { + return $c->render( + status => 403, + openapi => { + error => "Forbidden - anonymous users can only view public lists", + error_code => "forbidden" + } + ) unless $list->public; + + return $c->render( + status => 200, + openapi => $list->to_api + ); + } + + # For logged in users, check ownership and public status + unless ( $list->owner == $user->id || $list->public == 1 ) { + return $c->render( + status => 403, + openapi => { + error => "Forbidden - you can only view your own lists or public lists", + error_code => "forbidden" + } + ); + } + + return $c->render( + status => 200, + openapi => $list->to_api + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 public_update + +Update a public virtual shelf or a shelf you own + +=cut + +sub public_update { + my $c = shift->openapi->valid_input or return; + + my $user = $c->stash('koha.user'); + + # Handle anonymous users first + unless ($user) { + return $c->render( + status => 401, + openapi => { + error => "Authentication required", + error_code => "authentication_required" + } + ); + } + + my $list = Koha::Virtualshelves->find( $c->param('list_id') ); + + return $c->render_resource_not_found("List") + unless $list; + + if ( $list->owner != $user->id ) { + return $c->render( + status => 403, + openapi => { + error => "Forbidden - you can only update your own lists", + error_code => "forbidden" + } + ); + } + + if ( $list->allow_change_from_owner == 0 ) { + return $c->render( + status => 403, + openapi => { + error => "Forbidden - you can't update this list", + error_code => "forbidden" + } + ); + } + + return try { + $list->set_from_api( $c->req->json ); + $list->store(); + + return $c->render( + status => 200, + openapi => $list->to_api + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 public_delete + +Delete a public virtual shelf you own + +=cut + +sub public_delete { + my $c = shift->openapi->valid_input or return; + + my $user = $c->stash('koha.user'); + + # Handle anonymous users first + unless ($user) { + return $c->render( + status => 401, + openapi => { + error => "Authentication required", + error_code => "authentication_required" + } + ); + } + + my $list = Koha::Virtualshelves->find( $c->param('list_id') ); + + return $c->render_resource_not_found("List") + unless $list; + + # Check ownership + if ( $list->owner != $user->id ) { + return $c->render( + status => 403, + openapi => { + error => "Forbidden - you can only delete your own lists", + error_code => "forbidden" + } + ); + } + + # Check if modifications allowed + if ( $list->allow_change_from_owner == 0 ) { + return $c->render( + status => 403, + openapi => { + error => "Forbidden - you can't delete this list", + error_code => "forbidden" + } + ); + } + + return try { + $list->delete; + return $c->render_resource_deleted; + } catch { + $c->unhandled_exception($_); + }; +} + =head3 list_public =cut @@ -73,5 +418,4 @@ sub list_public { $c->unhandled_exception($_); }; } - 1; diff --git a/api/v1/swagger/definitions/list.yaml b/api/v1/swagger/definitions/list.yaml index 215a8af5dcf..34d495eed05 100644 --- a/api/v1/swagger/definitions/list.yaml +++ b/api/v1/swagger/definitions/list.yaml @@ -2,8 +2,9 @@ type: object properties: list_id: - description: Interal identifier for the list + description: Internal identifier for the list type: integer + readOnly: true name: description: List name type: string @@ -11,25 +12,39 @@ properties: description: Date the list was created type: string format: date-time + readOnly: true updated_on_date: description: Date the list was last updated type: string format: date-time + readOnly: true owner_id: description: Internal identifier for the owning patron type: integer allow_change_from_owner: description: If the owner can change the contents type: boolean + default: true allow_change_from_others: description: If others can change the contents type: boolean + default: false public: description: If the list is public type: boolean + default: true default_sort_field: description: The field this list is sorted on by default type: string + default: 'title' + allow_change_from_permitted_staff: + description: If only permitted staff can change the contents + type: boolean + default: false + allow_change_from_staff: + description: If staff can change the contents + type: boolean + default: false additionalProperties: false required: - list_id diff --git a/api/v1/swagger/definitions/public_list.yaml b/api/v1/swagger/definitions/public_list.yaml new file mode 100644 index 00000000000..255b9aa952a --- /dev/null +++ b/api/v1/swagger/definitions/public_list.yaml @@ -0,0 +1,57 @@ +--- +type: object +properties: + list_id: + description: Internal identifier for the list + type: integer + readOnly: true + name: + description: List name + type: string + creation_date: + description: Date the list was created + type: string + format: date-time + readOnly: true + updated_on_date: + description: Date the list was last updated + type: string + format: date-time + readOnly: true + owner_id: + description: Internal identifier for the owning patron + type: integer + readOnly: true + allow_change_from_owner: + description: If the owner can change the contents + type: boolean + default: true + allow_change_from_others: + description: If others can change the contents + type: boolean + default: false + public: + description: If the list is public + type: boolean + default: true + default_sort_field: + description: The field this list is sorted on by default + type: string + default: 'title' + allow_change_from_permitted_staff: + description: If only permitted staff can change the contents + type: boolean + default: false + allow_change_from_staff: + description: If staff can change the contents + type: boolean + default: false +additionalProperties: false +required: + - list_id + - name + - creation_date + - updated_on_date + - allow_change_from_owner + - allow_change_from_others + - public diff --git a/api/v1/swagger/paths/lists.yaml b/api/v1/swagger/paths/lists.yaml index 596c1ef314b..8fc0e02b79c 100644 --- a/api/v1/swagger/paths/lists.yaml +++ b/api/v1/swagger/paths/lists.yaml @@ -1,12 +1,17 @@ --- -"/public/lists": +/lists: get: x-mojo-to: Lists#list_public - operationId: listListsPublic description: "This resource returns a list of existing bibliographic lists." - summary: List bibliographic lists + x-koha-authorization: + permissions: + tools: manage_patron_lists + operationId: listLists tags: - lists + summary: List bibliographic lists + produces: + - application/json parameters: - name: only_mine in: query @@ -25,8 +30,6 @@ - $ref: "../swagger.yaml#/parameters/q_param" - $ref: "../swagger.yaml#/parameters/q_body" - $ref: "../swagger.yaml#/parameters/request_id_header" - produces: - - application/json responses: "200": description: A list of lists @@ -40,7 +43,6 @@ * `bad_request` * `invalid_query` - * `only_mine_forbidden` schema: $ref: "../swagger.yaml#/definitions/error" "403": @@ -58,3 +60,186 @@ description: Under maintenance schema: $ref: "../swagger.yaml#/definitions/error" + + + post: + x-mojo-to: Lists#add + x-koha-authorization: + permissions: + tools: manage_patron_lists + operationId: createList + tags: + - lists + description: "This resource creates a bibliographic list owned by a specified user." + summary: Create a new list + produces: + - application/json + parameters: + - name: body + in: body + description: A JSON object containing information about the new list + required: true + schema: + $ref: "../swagger.yaml#/definitions/list" + responses: + "201": + description: List added + schema: + $ref: "../swagger.yaml#/definitions/list" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + +/lists/{list_id}: + get: + x-mojo-to: Lists#public_get + x-koha-authorization: + permissions: + tools: manage_patron_lists + description: "This resource returns information on a specific existing bibliographic list." + operationId: readList + tags: + - lists + summary: Retrieve a specific list + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/list_id_pp" + responses: + "200": + description: OK + schema: + $ref: "../swagger.yaml#/definitions/list" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + + put: + x-mojo-to: Lists#update + x-koha-authorization: + permissions: + tools: manage_patron_lists + operationId: updateList + description: 'Update a specific list' + tags: + - lists + summary: Update a specific list + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/list_id_pp" + - name: body + in: body + description: A list object + required: true + schema: + $ref: "../swagger.yaml#/definitions/list" + responses: + "200": + description: A city + schema: + $ref: "../swagger.yaml#/definitions/list" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + + delete: + x-mojo-to: Lists#delete + x-koha-authorization: + permissions: + tools: manage_patron_lists + operationId: deleteList + tags: + - lists + summary: Delete a specific list + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/list_id_pp" + responses: + "204": + description: List deleted + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: List not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: Internal error + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" diff --git a/api/v1/swagger/paths/public_lists.yaml b/api/v1/swagger/paths/public_lists.yaml new file mode 100644 index 00000000000..6545bd6bd67 --- /dev/null +++ b/api/v1/swagger/paths/public_lists.yaml @@ -0,0 +1,229 @@ +--- +"/public/lists": + get: + x-mojo-to: Lists#list_public + operationId: listListsPublic + description: "This resource returns a list of existing bibliographic lists." + summary: List bibliographic lists + tags: + - lists + parameters: + - name: only_mine + in: query + description: Only return the users' lists + required: false + type: string + - name: only_public + in: query + description: Only return public lists + required: false + type: string + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + produces: + - application/json + responses: + "200": + description: A list of lists + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/public_list" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `bad_request` + * `invalid_query` + * `only_mine_forbidden` + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + + post: + x-mojo-to: Lists#public_add + operationId: createPublicList + tags: + - lists + summary: Create a new list you own + description: "This resource creates a bibliographic list owned by the authenticated user." + produces: + - application/json + parameters: + - name: body + in: body + description: A JSON object containing information about the new list + required: true + schema: + $ref: "../swagger.yaml#/definitions/public_list" + responses: + "201": + description: List added + schema: + $ref: "../swagger.yaml#/definitions/public_list" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + +/public/lists/{list_id}: + get: + x-mojo-to: Lists#public_get + operationId: getPublicList + description: "This resource returns information on a specific existing bibliographic list." + tags: + - lists + summary: Retrieve a specific public list or a list you own + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/list_id_pp" + responses: + "200": + description: OK + schema: + $ref: "../swagger.yaml#/definitions/public_list" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + put: + x-mojo-to: Lists#public_update + operationId: updatePublicList + tags: + - lists + description: 'Update a specific list that you own' + summary: Update a specific list you own + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/list_id_pp" + - name: body + in: body + description: A list object + required: true + schema: + $ref: "../swagger.yaml#/definitions/list" + responses: + "200": + description: A list + schema: + $ref: "../swagger.yaml#/definitions/list" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + + delete: + x-mojo-to: Lists#public_delete + operationId: deletePublicList + tags: + - lists + summary: Delete a specific list you own + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/list_id_pp" + responses: + "204": + description: List deleted + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: List not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: Internal error + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 45a062c9bf7..c439ccab1e8 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -158,6 +158,8 @@ definitions: $ref: ./definitions/preservation_train_item.yaml preservation_processing: $ref: ./definitions/preservation_processing.yaml + public_list: + $ref: ./definitions/public_list.yaml quote: $ref: ./definitions/quote.yaml recall: @@ -449,6 +451,10 @@ paths: $ref: "./paths/libraries.yaml#/~1libraries~1{library_id}~1cash_registers" "/libraries/{library_id}/desks": $ref: "./paths/libraries.yaml#/~1libraries~1{library_id}~1desks" + /lists: + $ref: ./paths/lists.yaml#/~1lists + "/lists/{list_id}": + $ref: "./paths/lists.yaml#/~1lists~1{list_id}" "/oauth/login/{provider_code}/{interface}": $ref: ./paths/oauth.yaml#/~1oauth~1login~1{provider_code}~1{interface} /oauth/token: @@ -516,7 +522,9 @@ paths: "/public/libraries/{library_id}": $ref: "./paths/libraries.yaml#/~1public~1libraries~1{library_id}" "/public/lists": - $ref: "./paths/lists.yaml#/~1public~1lists" + $ref: "./paths/public_lists.yaml#/~1public~1lists" + "/public/lists/{list_id}": + $ref: "./paths/public_lists.yaml#/~1public~1lists~1{list_id}" "/public/oauth/login/{provider_code}/{interface}": $ref: ./paths/public_oauth.yaml#/~1public~1oauth~1login~1{provider_code}~1{interface} "/public/patrons/{patron_id}/article_requests/{article_request_id}": @@ -810,6 +818,12 @@ parameters: name: license_id required: true type: integer + list_id_pp: + description: list internal identifier + in: path + name: list_id + required: true + type: integer match: description: Matching criteria enum: diff --git a/debian/scripts/koha-upgrade-schema b/debian/scripts/koha-upgrade-schema index 66a52b0f667..01d9b696629 100755 --- a/debian/scripts/koha-upgrade-schema +++ b/debian/scripts/koha-upgrade-schema @@ -41,12 +41,15 @@ for name in "$@" do if is_instance $name; then echo "Upgrading database schema for $name" - KOHA_CONF="/etc/koha/sites/$name/koha-conf.xml" \ + if KOHA_CONF="/etc/koha/sites/$name/koha-conf.xml" \ PERL5LIB=$PERL5LIB \ - "$CGI_PATH/installer/data/mysql/needs_update.pl" && \ - KOHA_CONF="/etc/koha/sites/$name/koha-conf.xml" \ - PERL5LIB=$PERL5LIB \ - "$CGI_PATH/installer/data/mysql/updatedatabase.pl" + "$CGI_PATH/installer/data/mysql/needs_update.pl"; then + KOHA_CONF="/etc/koha/sites/$name/koha-conf.xml" \ + PERL5LIB=$PERL5LIB \ + "$CGI_PATH/installer/data/mysql/updatedatabase.pl" + else + echo "No database change required" + fi else die "Error: Invalid instance name $name" fi diff --git a/t/db_dependent/api/v1/lists.t b/t/db_dependent/api/v1/lists.t new file mode 100755 index 00000000000..b0a4cb4de60 --- /dev/null +++ b/t/db_dependent/api/v1/lists.t @@ -0,0 +1,534 @@ +#!/usr/bin/env perl + +use Modern::Perl; + +use Test::More tests => 5; +use Test::Mojo; +use Data::Dumper; + +use JSON qw(encode_json); + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::Virtualshelves; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +my $t = Test::Mojo->new('Koha::REST::V1'); +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +subtest 'list() tests' => sub { + plan tests => 7; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + + # Create librarian with necessary permissions + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**13 } + } + ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + $librarian->set_password( { password => $password, skip_validation => 1 } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + + my $librarian_userid = $librarian->userid; + my $patron_userid = $patron->userid; + + # Create test lists owned by different users + my $list_1 = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { owner => $librarian->id, public => 1 } + } + ); + my $list_2 = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { owner => $librarian->id, public => 0 } + } + ); + + my $q = encode_json( { list_id => { -in => [ $list_1->id, $list_2->id, ] } } ); + + # Test unauthorized access + $t->get_ok("/api/v1/lists?q=$q")->status_is( 401, "Anonymous users cannot access admin lists endpoint" ); + + $t->get_ok("//$patron_userid:$password@/api/v1/lists?q=$q") + ->status_is( 403, "Regular patrons cannot access admin lists endpoint" ); + + # Test authorized access - use to_api method which is already tested + my $expected = [ $list_1->to_api, $list_2->to_api ]; + + $t->get_ok("//$librarian_userid:$password@/api/v1/lists?q=$q")->status_is(200)->json_is($expected); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + plan tests => 11; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + + # Create librarian with necessary permissions + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**13 } + } + ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $another_patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + $librarian->set_password( { password => $password, skip_validation => 1 } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + + my $librarian_userid = $librarian->userid; + my $patron_userid = $patron->userid; + + # Create test lists with different attributes + my $public_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $librarian->id, + public => 1, + allow_change_from_owner => 1 + } + } + ); + + my $private_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 0, + allow_change_from_owner => 0 + } + } + ); + + my $another_private_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $another_patron->id, + public => 0, + allow_change_from_owner => 0 + } + } + ); + + # Test unauthorized access + $t->get_ok( "/api/v1/lists/" . $public_list->id ) + ->status_is( 401, "Anonymous users cannot access admin lists endpoint" ); + + # Test access without permission + $t->get_ok( "//$patron_userid:$password@/api/v1/lists/" . $public_list->id ) + ->status_is( 403, "Regular patrons cannot access admin lists endpoint" ); + + # Test authorized access to public list + $t->get_ok( "//$librarian_userid:$password@/api/v1/lists/" . $public_list->id )->status_is(200) + ->json_is( $public_list->to_api ); + + # Test authorized access to another patron's private list + $t->get_ok( "//$librarian_userid:$password@/api/v1/lists/" . $private_list->id )->status_is( + 403, + "Librarian cannot access private lists they don't own" + ); + + # Test non-existent list + $t->get_ok("//$librarian_userid:$password@/api/v1/lists/99999999")->status_is( 404, "List not found" ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + plan tests => 16; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + + # Create librarian with necessary permissions + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**13 } + } + ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $another_patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + $librarian->set_password( { password => $password, skip_validation => 1 } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + + my $librarian_userid = $librarian->userid; + my $patron_userid = $patron->userid; + + # Test unauthorized access + my $list_data = { + name => "Test List", + owner_id => $librarian->id, + public => 1, + allow_change_from_owner => 1, allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->post_ok( "/api/v1/lists" => json => $list_data )->status_is( 401, "Anonymous users cannot create lists" ); + + $t->post_ok( "//$patron_userid:$password@/api/v1/lists" => json => $list_data ) + ->status_is( 403, "Regular patrons cannot create lists" ); + + # Test authorized creation - list for self + $t->post_ok( "//$librarian_userid:$password@/api/v1/lists" => json => $list_data )->status_is(201)->header_like( + Location => qr|^/api/v1/lists/\d+|, + 'Location header is correct' + )->json_has( '/list_id', 'List ID is present in response' ) + ->json_has( '/name', 'List name is present in response' ); + + # Test authorized creation - list for another patron + my $list_for_patron = { + name => "Test List for Patron", + owner_id => $another_patron->id, + public => 0, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->post_ok( "//$librarian_userid:$password@/api/v1/lists" => json => $list_for_patron )->status_is(201)->json_is( + '/owner_id' => $another_patron->id, + 'List created with correct owner' + )->json_is( '/name' => 'Test List for Patron', 'List name set correctly' ); + + # Test creating list with invalid owner_id + my $list_invalid_owner = { + name => "Test List", + owner_id => 999999, # Non-existent patron id + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->post_ok( "//$librarian_userid:$password@/api/v1/lists" => json => $list_invalid_owner )->status_is(400) + ->json_like( '/error' => qr/Invalid owner_id/ ); + + $schema->storage->txn_rollback; +}; + +subtest 'update() tests' => sub { + plan tests => 27; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + + # Create librarian with necessary permissions + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**13 } + } + ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $another_patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + $librarian->set_password( { password => $password, skip_validation => 1 } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + + my $librarian_userid = $librarian->userid; + my $patron_userid = $patron->userid; + + # Create test list + my $list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $librarian->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0 + } + } + ); + + # Test unauthorized access + my $update_data = { + name => "Updated List Name 1", + public => 0, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->put_ok( "/api/v1/lists/" . $list->id => json => $update_data ) + ->status_is( 401, "Anonymous users cannot update lists" ); + + $t->put_ok( "//$patron_userid:$password@/api/v1/lists/" . $list->id => json => $update_data ) + ->status_is( 403, "Regular patrons cannot update lists" ); + + # Test successful update + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/" . $list->id => json => $update_data )->status_is(200) + ->json_is( + '/name' => 'Updated List Name 1', + 'List name updated correctly' + )->json_is( '/public' => 0, 'List privacy updated correctly' ); + + # Test update of non-existent list + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/99999999" => json => $update_data ) + ->status_is( 404, "Attempting to update non-existent list returns 404" ); + + # Test partial update - changed to include all required fields + my $partial_update = { + name => "Updated List Name 2", + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/" . $list->id => json => $partial_update )->status_is(200) + ->json_is( '/name' => "Updated List Name 2", "Update successful" ); + + # Test updating another patron's list with librarian permissions + my $patron_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 1, + allow_change_from_staff => 1 + } + } + ); + + my $update_data_2 = { + name => "Updated List Name 3", + public => 0, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/" . $patron_list->id => json => $update_data_2 ) + ->status_is(200)->json_is( '/name' => 'Updated List Name 3', 'Librarian can update other patron\'s list' ); + + # Test librarian updating their own list + my $librarian_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $librarian->id, + public => 1, + allow_change_from_owner => 0 # Even librarian should respect this flag for their own lists + } + } + ); + + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/" . $librarian_list->id => json => $update_data ) + ->status_is( + 403, + "Even librarians must respect allow_change_from_owner for their own lists" + ); + + # Test update with allow_change_from_staff permission + my $list_staff = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + allow_change_from_staff => 1 + } + } + ); + + my $update_data_3 = { + name => "Updated List Name 4", + public => 0, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/" . $list_staff->id => json => $update_data_3 ) + ->status_is(200) + ->json_is( '/name' => 'Updated List Name 4', 'Staff can update list with allow_change_from_staff' ); + + # Test updating list with no permissions + my $no_permission_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + allow_change_from_staff => 0 + } + } + ); + + my $update_data_4 = { + name => "Updated List Name 5", + public => 0, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/" . $no_permission_list->id => json => $update_data_4 ) + ->status_is(403)->json_is( '/error' => 'Cannot modify list without proper permissions' ); + + # Test update when both permission flags are false + my $no_perm_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + allow_change_from_staff => 0 + } + } + ); + + $t->put_ok( "//$librarian_userid:$password@/api/v1/lists/" . $no_perm_list->id => json => $update_data ) + ->status_is(403)->json_is( '/error' => 'Cannot modify list without proper permissions' ); + + $schema->storage->txn_rollback; +}; + +subtest 'delete() tests' => sub { + plan tests => 16; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + + # Create librarian with necessary permissions + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**13 } + } + ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + $librarian->set_password( { password => $password, skip_validation => 1 } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + + my $librarian_userid = $librarian->userid; + my $patron_userid = $patron->userid; + + # Create test list + my $list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $librarian->id, + allow_change_from_staff => 1 + } + } + ); + + # Test list for deleting another patron's list with librarian permissions + my $patron_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + allow_change_from_staff => 1 + } + } + ); + + # Test unauthorized access + $t->delete_ok( "/api/v1/lists/" . $list->id )->status_is( 401, "Anonymous users cannot delete lists" ); + + $t->delete_ok( "//$patron_userid:$password@/api/v1/lists/" . $list->id ) + ->status_is( 403, "Regular patrons cannot delete lists" ); + + # Test successful delete + $t->delete_ok( "//$librarian_userid:$password@/api/v1/lists/" . $list->id ) + ->status_is( 204, "List deleted successfully" ); + + # Test non-existent list + $t->delete_ok("//$librarian_userid:$password@/api/v1/lists/99999999") + ->status_is( 404, "Attempting to delete non-existent list returns 404" ); + + $t->delete_ok( "//$librarian_userid:$password@/api/v1/lists/" . $patron_list->id ) + ->status_is( 204, "Librarian can delete other patron's list" ); + + # Test deleting list with allow_change_from_staff permission + my $list_staff = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + allow_change_from_staff => 1 + } + } + ); + + $t->delete_ok( "//$librarian_userid:$password@/api/v1/lists/" . $list_staff->id ) + ->status_is( 204, "Staff can delete list with allow_change_from_staff" ); + + # Test deleting list with allow_change_from_others permission + my $list_others = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 1, + allow_change_from_staff => 0 + } + } + ); + + $t->delete_ok( "//$librarian_userid:$password@/api/v1/lists/" . $list_others->id ) + ->status_is( 204, "Can delete list with allow_change_from_others" ); + + # Test deleting list with no permissions + my $no_permission_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + allow_change_from_staff => 0 + } + } + ); + + $t->delete_ok( "//$librarian_userid:$password@/api/v1/lists/" . $no_permission_list->id ) + ->status_is( 403, "Cannot delete list without proper permissions" ); + + $schema->storage->txn_rollback; +}; + +1; diff --git a/t/db_dependent/api/v1/public/lists.t b/t/db_dependent/api/v1/public/lists.t old mode 100755 new mode 100644 index 2554b9f902d..95afc8b69b3 --- a/t/db_dependent/api/v1/public/lists.t +++ b/t/db_dependent/api/v1/public/lists.t @@ -18,7 +18,7 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 2; +use Test::More tests => 6; use Test::Mojo; use JSON qw(encode_json); @@ -52,73 +52,397 @@ subtest 'list_public() tests' => sub { $patron_2->set_password( { password => $password, skip_validation => 1 } ); my $patron_2_userid = $patron_2->userid; - my $list_1 = - $builder->build_object( { class => 'Koha::Virtualshelves', value => { owner => $patron_1->id, public => 1 } } ); - my $list_2 = - $builder->build_object( { class => 'Koha::Virtualshelves', value => { owner => $patron_1->id, public => 0 } } ); - my $list_3 = - $builder->build_object( { class => 'Koha::Virtualshelves', value => { owner => $patron_2->id, public => 1 } } ); - my $list_4 = - $builder->build_object( { class => 'Koha::Virtualshelves', value => { owner => $patron_2->id, public => 0 } } ); - - my $q = encode_json( + my $list_1 = $builder->build_object( { - list_id => { - -in => [ - $list_1->id, - $list_2->id, - $list_3->id, - $list_4->id, - ] - } + class => 'Koha::Virtualshelves', + value => { owner => $patron_1->id, public => 1 } + } + ); + my $list_2 = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { owner => $patron_1->id, public => 0 } + } + ); + my $list_3 = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { owner => $patron_2->id, public => 1 } + } + ); + my $list_4 = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { owner => $patron_2->id, public => 0 } } ); + my $q = encode_json( { list_id => { -in => [ $list_1->id, $list_2->id, $list_3->id, $list_4->id, ] } } ); + # anonymous - $t->get_ok("/api/v1/public/lists?q=$q")->status_is( 200, "Anonymous users can only fetch public lists" ) - ->json_is( [ $list_1->to_api( { public => 1 } ), $list_3->to_api( { public => 1 } ) ] ); + $t->get_ok("/api/v1/public/lists?q=$q")->status_is( 200, "Anonymous users can only fetch public lists" )->json_is( + [ + $list_1->to_api( { public => 1 } ), + $list_3->to_api( { public => 1 } ) + ] + ); $t->get_ok("/api/v1/public/lists?q=$q&only_public=1") - ->status_is( 200, "Anonymous users can only fetch public lists" ) - ->json_is( [ $list_1->to_api( { public => 1 } ), $list_3->to_api( { public => 1 } ) ] ); + ->status_is( 200, "Anonymous users can only fetch public lists" )->json_is( + [ + $list_1->to_api( { public => 1 } ), + $list_3->to_api( { public => 1 } ) + ] + ); - $t->get_ok("/api/v1/public/lists?q=$q&only_mine=1") - ->status_is( 400, "Passing only_mine on an anonymous session generates a 400 code" ) - ->json_is( '/error_code' => q{only_mine_forbidden} ); + $t->get_ok("/api/v1/public/lists?q=$q&only_mine=1")->status_is( + 400, + "Passing only_mine on an anonymous session generates a 400 code" + )->json_is( '/error_code' => q{only_mine_forbidden} ); - $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q&only_mine=1") - ->status_is( 200, "Passing only_mine with a logged in user makes it return only their lists" ) - ->json_is( [ $list_1->to_api( { public => 1 } ), $list_2->to_api( { public => 1 } ) ] ); + $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q&only_mine=1")->status_is( + 200, + "Passing only_mine with a logged in user makes it return only their lists" + )->json_is( + [ + $list_1->to_api( { public => 1 } ), + $list_2->to_api( { public => 1 } ) + ] + ); - $t->get_ok("//$patron_2_userid:$password@/api/v1/public/lists?q=$q&only_mine=1") - ->status_is( 200, "Passing only_mine with a logged in user makes it return only their lists" ) - ->json_is( [ $list_3->to_api( { public => 1 } ), $list_4->to_api( { public => 1 } ) ] ); + $t->get_ok("//$patron_2_userid:$password@/api/v1/public/lists?q=$q&only_mine=1")->status_is( + 200, + "Passing only_mine with a logged in user makes it return only their lists" + )->json_is( + [ + $list_3->to_api( { public => 1 } ), + $list_4->to_api( { public => 1 } ) + ] + ); # only public - $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q&only_public=1") - ->status_is( 200, "Passing only_public with a logged in user makes it return only public lists" ) - ->json_is( [ $list_1->to_api( { public => 1 } ), $list_3->to_api( { public => 1 } ) ] ); - - $t->get_ok("//$patron_2_userid:$password@/api/v1/public/lists?q=$q&only_public=1") - ->status_is( 200, "Passing only_public with a logged in user makes it return only public lists" ) - ->json_is( [ $list_1->to_api( { public => 1 } ), $list_3->to_api( { public => 1 } ) ] ); - - $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q") - ->status_is( 200, "Not filtering with only_mine or only_public makes it return all accessible lists" ) - ->json_is( - [ $list_1->to_api( { public => 1 } ), $list_2->to_api( { public => 1 } ), $list_3->to_api( { public => 1 } ) ] + $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q&only_public=1")->status_is( + 200, + "Passing only_public with a logged in user makes it return only public lists" + )->json_is( + [ + $list_1->to_api( { public => 1 } ), + $list_3->to_api( { public => 1 } ) + ] + ); + + $t->get_ok("//$patron_2_userid:$password@/api/v1/public/lists?q=$q&only_public=1")->status_is( + 200, + "Passing only_public with a logged in user makes it return only public lists" + )->json_is( + [ + $list_1->to_api( { public => 1 } ), + $list_3->to_api( { public => 1 } ) + ] + ); + + $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q")->status_is( + 200, + "Not filtering with only_mine or only_public makes it return all accessible lists" + )->json_is( + [ + $list_1->to_api( { public => 1 } ), + $list_2->to_api( { public => 1 } ), + $list_3->to_api( { public => 1 } ) + ] + ); + + $t->get_ok("//$patron_2_userid:$password@/api/v1/public/lists?q=$q")->status_is( + 200, + "Not filtering with only_mine or only_public makes it return all accessible lists" + )->json_is( + [ + $list_1->to_api( { public => 1 } ), + $list_3->to_api( { public => 1 } ), + $list_4->to_api( { public => 1 } ) + ] + ); + + # conflicting params + $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q&only_public=1&only_mine=1")->status_is( + 200, + "Passing only_public with a logged in user makes it return only public lists" + )->json_is( [ $list_1->to_api( { public => 1 } ) ] ); + + $schema->storage->txn_rollback; +}; + +subtest 'public_get() tests' => sub { + plan tests => 12; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } ); + my $patron_2 = $builder->build_object( { class => 'Koha::Patrons' } ); + + $patron_1->set_password( { password => $password, skip_validation => 1 } ); + $patron_2->set_password( { password => $password, skip_validation => 1 } ); + + my $patron_1_userid = $patron_1->userid; + my $patron_2_userid = $patron_2->userid; + + # Create test lists + my $public_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron_1->id, + public => 1, + allow_change_from_owner => 1 + } + } + ); + + my $private_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron_1->id, + public => 0, + allow_change_from_owner => 0 + } + } + ); + + # Test anonymous access to public list + $t->get_ok( "/api/v1/public/lists/" . $public_list->id )->status_is(200)->json_is( $public_list->to_api ); + + # Test anonymous access to private list + $t->get_ok( "/api/v1/public/lists/" . $private_list->id ) + ->status_is( 403, 'Anonymous user cannot access private list' ); + + # Test authenticated access - owner can see their private list + $t->get_ok( "//$patron_1_userid:$password@/api/v1/public/lists/" . $private_list->id )->status_is(200) + ->json_is( $private_list->to_api ); + + # Test non-owner access to private list + $t->get_ok( "//$patron_2_userid:$password@/api/v1/public/lists/" . $private_list->id ) + ->status_is( 403, 'Non-owner cannot access private list' ); + + # Test non-existent list + $t->get_ok("/api/v1/public/lists/99999999")->status_is( 404, "List not found" ); + + $schema->storage->txn_rollback; +}; + +subtest 'public_add() tests' => sub { + plan tests => 9; # Reduced test count since we're removing one assertion + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $another_patron = $builder->build_object( { class => 'Koha::Patrons' } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $patron_userid = $patron->userid; + + my $list_data = { + name => "Test List", + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + # Test anonymous attempt + $t->post_ok( "/api/v1/public/lists" => json => $list_data )->status_is( 401, 'Anonymous user cannot create list' ); + + # Test authenticated user can create list + $t->post_ok( "//$patron_userid:$password@/api/v1/public/lists" => json => $list_data )->status_is(201) + ->json_has( '/list_id', 'List ID is present in response' ) + ->json_has( '/name', 'List name is present in response' )->json_is( + '/owner_id' => $patron->id, + 'List created with logged in user as owner' ); - $t->get_ok("//$patron_2_userid:$password@/api/v1/public/lists?q=$q") - ->status_is( 200, "Not filtering with only_mine or only_public makes it return all accessible lists" ) - ->json_is( - [ $list_1->to_api( { public => 1 } ), $list_3->to_api( { public => 1 } ), $list_4->to_api( { public => 1 } ) ] + # Test attempt to specify owner_id + my $list_with_owner = { + name => "Test List", + owner_id => $another_patron->id, # Should be rejected + public => 1, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + $t->post_ok( "//$patron_userid:$password@/api/v1/public/lists" => json => $list_with_owner )->status_is(400); + + $schema->storage->txn_rollback; +}; + +subtest 'public_update() tests' => sub { + plan tests => 15; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $another_patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $another_patron->set_password( { password => $password, skip_validation => 1 } ); + + my $patron_userid = $patron->userid; + my $another_patron_userid = $another_patron->userid; + + # Create a list that can be modified + my $modifiable_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1 + } + } + ); + + # Create a list that cannot be modified + my $unmodifiable_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 0 + } + } + ); + + my $update_data = { + name => "Updated List Name", + public => 0, + allow_change_from_owner => 1, + allow_change_from_others => 0, + default_sort_field => 'title' + }; + + # Test anonymous attempt + $t->put_ok( "/api/v1/public/lists/" . $modifiable_list->id => json => $update_data ) + ->status_is( 401, 'Anonymous user cannot update list' ); + + # Test non-owner update attempt + $t->put_ok( + "//$another_patron_userid:$password@/api/v1/public/lists/" . $modifiable_list->id => json => $update_data ) + ->status_is( 403, 'Non-owner cannot update list' ); + + # Test owner update success + $t->put_ok( "//$patron_userid:$password@/api/v1/public/lists/" . $modifiable_list->id => json => $update_data ) + ->status_is(200)->json_is( '/name' => 'Updated List Name', 'List name updated' ) + ->json_is( '/public' => 0, 'List privacy updated' ); + + # Test update of non-existent list + $t->put_ok( "//$patron_userid:$password@/api/v1/public/lists/99999999" => json => $update_data ) + ->status_is( 404, "List not found" ); + + # Test update of unmodifiable list + $t->put_ok( "//$patron_userid:$password@/api/v1/public/lists/" . $unmodifiable_list->id => json => $update_data ) + ->status_is( + 403, + "Cannot update list when allow_change_from_owner is false" + )->json_is( '/error_code' => 'forbidden', 'Correct error code returned' ); + + # Create librarian with necessary permissions + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**13 } + } + ); + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $librarian_userid = $librarian->userid; + + # Test librarian using public endpoint + $t->put_ok( "//$librarian_userid:$password@/api/v1/public/lists/" . $modifiable_list->id => json => $update_data ) + ->status_is( + 403, + 'Librarian must use admin endpoint to modify others lists' ); - # conflicting params - $t->get_ok("//$patron_1_userid:$password@/api/v1/public/lists?q=$q&only_public=1&only_mine=1") - ->status_is( 200, "Passing only_public with a logged in user makes it return only public lists" ) - ->json_is( [ $list_1->to_api( { public => 1 } ) ] ); + $schema->storage->txn_rollback; +}; + +subtest 'public_delete() tests' => sub { + plan tests => 12; + + $schema->storage->txn_begin; + + my $password = 'thePassword123'; + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $another_patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $another_patron->set_password( { password => $password, skip_validation => 1 } ); + + my $patron_userid = $patron->userid; + my $another_patron_userid = $another_patron->userid; + + # Create test lists for different scenarios + my $modifiable_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 1 + } + } + ); + + my $other_patron_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $another_patron->id, + public => 1, + allow_change_from_owner => 1 + } + } + ); + + my $unmodifiable_list = $builder->build_object( + { + class => 'Koha::Virtualshelves', + value => { + owner => $patron->id, + public => 1, + allow_change_from_owner => 0 + } + } + ); + + # Test anonymous attempt + $t->delete_ok( "/api/v1/public/lists/" . $modifiable_list->id ) + ->status_is( 401, 'Anonymous user cannot delete list' ); + + # Test non-owner delete attempt + $t->delete_ok( "//$another_patron_userid:$password@/api/v1/public/lists/" . $modifiable_list->id ) + ->status_is( 403, 'Non-owner cannot delete list' ); + + # Test attempt to delete another patron's list + $t->delete_ok( "//$patron_userid:$password@/api/v1/public/lists/" . $other_patron_list->id ) + ->status_is( 403, "Cannot delete another patron's list" ); + + # Test delete of unmodifiable list + $t->delete_ok( "//$patron_userid:$password@/api/v1/public/lists/" . $unmodifiable_list->id )->status_is( + 403, + "Cannot delete list when allow_change_from_owner is false" + ); + + # Test delete of non-existent list + $t->delete_ok("//$patron_userid:$password@/api/v1/public/lists/99999999")->status_is( 404, "List not found" ); + + # Test successful delete by owner + $t->delete_ok( "//$patron_userid:$password@/api/v1/public/lists/" . $modifiable_list->id ) + ->status_is( 204, 'List deleted successfully' ); $schema->storage->txn_rollback; }; + +1;