From f54e18fb230c8f5e5763c9a6b12760ae0ae852b1 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Tue, 25 Oct 2022 13:14:19 +0100 Subject: [PATCH] Bug 31028: Add API's for tickets This patch adds basic CRUD API's for the ticket endpoints. Signed-off-by: David Nind Signed-off-by: Helen Oliver --- Koha/REST/V1/Tickets.pm | 157 ++++++++++ Koha/Ticket.pm | 13 + api/v1/swagger/definitions/ticket.yaml | 59 ++++ api/v1/swagger/paths/tickets.yaml | 252 +++++++++++++++++ api/v1/swagger/swagger.yaml | 14 + t/db_dependent/api/v1/tickets.t | 377 +++++++++++++++++++++++++ 6 files changed, 872 insertions(+) create mode 100644 Koha/REST/V1/Tickets.pm create mode 100644 api/v1/swagger/definitions/ticket.yaml create mode 100644 api/v1/swagger/paths/tickets.yaml create mode 100755 t/db_dependent/api/v1/tickets.t diff --git a/Koha/REST/V1/Tickets.pm b/Koha/REST/V1/Tickets.pm new file mode 100644 index 00000000000..5fd1c0f4321 --- /dev/null +++ b/Koha/REST/V1/Tickets.pm @@ -0,0 +1,157 @@ +package Koha::REST::V1::Tickets; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::Ticket; +use Koha::Tickets; +use Koha::Ticket::Update; +use Koha::Ticket::Updates; + +use Try::Tiny qw( catch try ); + +=head1 API + +=head2 Methods + +=head3 list + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $tickets_set = Koha::Tickets->new; + my $tickets = $c->objects->search($tickets_set); + return $c->render( status => 200, openapi => $tickets ); + } + catch { + $c->unhandled_exception($_); + }; + +} + +=head3 get + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $ticket = Koha::Tickets->find( $c->validation->param('ticket_id') ); + unless ($ticket) { + return $c->render( + status => 404, + openapi => { error => "Ticket not found" } + ); + } + + return $c->render( status => 200, openapi => $ticket->to_api ); + } + catch { + $c->unhandled_exception($_); + } +} + +=head3 add + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + my $patron = $c->stash('koha.user'); + + return try { + my $body = $c->validation->param('body'); + + # Set reporter from session + $body->{reporter_id} = $patron->id; + # FIXME: We should allow impersonation at a later date to + # allow an API user to submit on behalf of a user + + my $ticket = Koha::Ticket->new_from_api($body)->store; + $ticket->discard_changes; + $c->res->headers->location( + $c->req->url->to_string . '/' . $ticket->id ); + return $c->render( + status => 201, + openapi => $ticket->to_api + ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 update + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $ticket = Koha::Tickets->find( $c->validation->param('ticket_id') ); + + if ( not defined $ticket ) { + return $c->render( + status => 404, + openapi => { error => "Object not found" } + ); + } + + return try { + $ticket->set_from_api( $c->validation->param('body') ); + $ticket->store(); + return $c->render( status => 200, openapi => $ticket->to_api ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 delete + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $ticket = Koha::Tickets->find( $c->validation->param('ticket_id') ); + if ( not defined $ticket ) { + return $c->render( + status => 404, + openapi => { error => "Object not found" } + ); + } + + return try { + $ticket->delete; + return $c->render( + status => 204, + openapi => q{} + ); + } + catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/Koha/Ticket.pm b/Koha/Ticket.pm index 408c818319a..55d177da2c7 100644 --- a/Koha/Ticket.pm +++ b/Koha/Ticket.pm @@ -99,6 +99,19 @@ sub add_update { =head2 Internal methods +=cut + +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::Ticket object +on the API. + +=cut + +sub to_api_mapping { + return { id => 'ticket_id', }; +} + =head3 _type =cut diff --git a/api/v1/swagger/definitions/ticket.yaml b/api/v1/swagger/definitions/ticket.yaml new file mode 100644 index 00000000000..06a64e867ae --- /dev/null +++ b/api/v1/swagger/definitions/ticket.yaml @@ -0,0 +1,59 @@ +--- +type: object +properties: + ticket_id: + type: integer + description: Internal ticket identifier + readOnly: true + reported_date: + type: + - string + - "null" + format: date-time + description: Date the ticket was reported + readOnly: true + biblio: + type: + - object + - "null" + description: The object representing the biblio the ticket is related to + readOnly: true + biblio_id: + type: integer + description: Internal identifier for the biblio the ticket is related to + title: + type: string + description: Ticket title + body: + type: string + description: Ticket details + reporter: + type: + - object + - "null" + description: The object representing the patron who reported the ticket + readOnly: true + reporter_id: + type: integer + description: Internal identifier for the patron who reported the ticket + resolver: + type: + - object + - "null" + description: The object representing the user who resolved the ticket + readOnly: true + resolver_id: + type: + - integer + - "null" + description: Internal identifier for the user who resolved the ticket + resolved_date: + type: + - string + - "null" + format: date-time + description: Date the ticket was resolved_date +additionalProperties: false +required: + - title + - body diff --git a/api/v1/swagger/paths/tickets.yaml b/api/v1/swagger/paths/tickets.yaml new file mode 100644 index 00000000000..26bf2afa446 --- /dev/null +++ b/api/v1/swagger/paths/tickets.yaml @@ -0,0 +1,252 @@ +--- +/tickets: + get: + x-mojo-to: Tickets#list + operationId: listTickets + tags: + - tickets + summary: List tickets + produces: + - application/json + parameters: + - $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/q_header" + - $ref: "../swagger.yaml#/parameters/request_id_header" + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - reporter + - resolver + - biblio + collectionFormat: csv + responses: + "200": + description: A list of tickets + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/ticket" + "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" + x-koha-authorization: + permissions: + catalogue: "1" + post: + x-mojo-to: Tickets#add + operationId: addTicket + tags: + - tickets + summary: Add ticket + parameters: + - name: body + in: body + description: A JSON object containing informations about the new ticket + required: true + schema: + $ref: "../swagger.yaml#/definitions/ticket" + produces: + - application/json + responses: + "201": + description: Ticket added + schema: + $ref: "../swagger.yaml#/definitions/ticket" + "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" + x-koha-authorization: + permissions: + catalogue: "1" +"/tickets/{ticket_id}": + get: + x-mojo-to: Tickets#get + operationId: getTicket + tags: + - tickets + summary: Get ticket + parameters: + - $ref: "../swagger.yaml#/parameters/ticket_id_pp" + produces: + - application/json + responses: + "200": + description: A ticket + schema: + $ref: "../swagger.yaml#/definitions/ticket" + "404": + description: Ticket 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" + x-koha-authorization: + permissions: + catalogue: "1" + put: + x-mojo-to: Tickets#update + operationId: updateTicket + tags: + - tickets + summary: Update ticket + parameters: + - $ref: "../swagger.yaml#/parameters/ticket_id_pp" + - name: body + in: body + description: A ticket object + required: true + schema: + $ref: "../swagger.yaml#/definitions/ticket" + produces: + - application/json + responses: + "200": + description: A ticket + schema: + $ref: "../swagger.yaml#/definitions/ticket" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Ticket 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" + x-koha-authorization: + permissions: + editcatalogue: edit_catalogue + delete: + x-mojo-to: Tickets#delete + operationId: deleteTicket + tags: + - tickets + summary: Delete ticket + parameters: + - $ref: "../swagger.yaml#/parameters/ticket_id_pp" + produces: + - application/json + responses: + "204": + description: Ticket deleted + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Ticket 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" + x-koha-authorization: + permissions: + editcatalogue: edit_catalogue +/public/tickets: + post: + x-mojo-to: Tickets#add + operationId: addTicketPublic + tags: + - tickets + summary: Add ticket + parameters: + - name: body + in: body + description: A JSON object containing informations about the new ticket + required: true + schema: + $ref: "../swagger.yaml#/definitions/ticket" + produces: + - application/json + responses: + "201": + description: Ticket added + schema: + $ref: "../swagger.yaml#/definitions/ticket" + "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" diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 696a73be8b4..9c763bd21d0 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -88,6 +88,8 @@ definitions: $ref: ./definitions/smtp_server.yaml suggestion: $ref: ./definitions/suggestion.yaml + ticket: + $ref: ./definitions/ticket.yaml transfer_limit: $ref: ./definitions/transfer_limit.yaml vendor: @@ -291,6 +293,8 @@ paths: $ref: "./paths/public_patrons.yaml#/~1public~1patrons~1{patron_id}~1guarantors~1can_see_checkouts" "/public/patrons/{patron_id}/password": $ref: "./paths/public_patrons.yaml#/~1public~1patrons~1{patron_id}~1password" + "/public/tickets": + $ref: "./paths/tickets.yaml#/~1public~1tickets" /quotes: $ref: ./paths/quotes.yaml#/~1quotes "/quotes/{quote_id}": @@ -311,6 +315,10 @@ paths: $ref: "./paths/suggestions.yaml#/~1suggestions~1{suggestion_id}" /suggestions/managers: $ref: paths/suggestions.yaml#/~1suggestions~1managers + "/tickets": + $ref: "./paths/tickets.yaml#/~1tickets" + "/tickets/{ticket_id}": + $ref: "./paths/tickets.yaml#/~1tickets~1{ticket_id}" /transfer_limits: $ref: ./paths/transfer_limits.yaml#/~1transfer_limits /transfer_limits/batch: @@ -563,6 +571,12 @@ parameters: name: suggestion_id required: true type: integer + ticket_id_pp: + description: Internal ticket identifier + in: path + name: ticket_id + required: true + type: integer transfer_limit_id_pp: description: Internal transfer limit identifier in: path diff --git a/t/db_dependent/api/v1/tickets.t b/t/db_dependent/api/v1/tickets.t new file mode 100755 index 00000000000..788d32a252e --- /dev/null +++ b/t/db_dependent/api/v1/tickets.t @@ -0,0 +1,377 @@ +#!/usr/bin/env perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 5; +use Test::Mojo; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::Tickets; +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 => 14; + + $schema->storage->txn_begin; + + Koha::Tickets->search->delete; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**2 } # catalogue flag = 2 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + ## Authorized user tests + # No tickets, so empty array should be returned + $t->get_ok("//$userid:$password@/api/v1/tickets")->status_is(200) + ->json_is( [] ); + + my $ticket = $builder->build_object( { class => 'Koha::Tickets' } ); + + # One ticket created, should get returned + $t->get_ok("//$userid:$password@/api/v1/tickets")->status_is(200) + ->json_is( [ $ticket->to_api ] ); + + my $another_ticket = $builder->build_object( { class => 'Koha::Tickets' } ); + my $and_another_ticket = + $builder->build_object( { class => 'Koha::Tickets' } ); + + # Two tickets created, they should both be returned + $t->get_ok("//$userid:$password@/api/v1/tickets")->status_is(200)->json_is( + [ + $ticket->to_api, $another_ticket->to_api, + $and_another_ticket->to_api + ] + ); + + # Warn on unsupported query parameter + $t->get_ok("//$userid:$password@/api/v1/tickets?ticket_blah=blah") + ->status_is(400)->json_is( + [ + { + path => '/query/ticket_blah', + message => 'Malformed query string' + } + ] + ); + + # Unauthorized access + $t->get_ok("//$unauth_userid:$password@/api/v1/tickets")->status_is(403); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + my $ticket = $builder->build_object( { class => 'Koha::Tickets' } ); + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**2 } # catalogue flag = 2 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/tickets/" . $ticket->id ) + ->status_is(200)->json_is( $ticket->to_api ); + + $t->get_ok( "//$unauth_userid:$password@/api/v1/tickets/" . $ticket->id ) + ->status_is(403); + + my $ticket_to_delete = + $builder->build_object( { class => 'Koha::Tickets' } ); + my $non_existent_id = $ticket_to_delete->id; + $ticket_to_delete->delete; + + $t->get_ok("//$userid:$password@/api/v1/tickets/$non_existent_id") + ->status_is(404)->json_is( '/error' => 'Ticket not found' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + + plan tests => 21; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**2 } # catalogue flag = 2 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $biblio = $builder->build_sample_biblio(); + my $ticket = { + biblio_id => $biblio->id, + title => "Test ticket", + body => "Test ticket details", + }; + + # Unauthorized attempt to write + $t->post_ok( + "//$unauth_userid:$password@/api/v1/tickets" => json => $ticket ) + ->status_is(403); + + # Authorized attempt to write invalid data + my $ticket_with_invalid_field = { + blah => "Something wrong", + biblio_id => $biblio->id, + title => "Test ticket", + body => "Test ticket details", + }; + + $t->post_ok( "//$userid:$password@/api/v1/tickets" => json => + $ticket_with_invalid_field )->status_is(400)->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + # Authorized attempt to write + my $ticket_id = + $t->post_ok( "//$userid:$password@/api/v1/tickets" => json => $ticket ) + ->status_is( 201, 'SWAGGER3.2.1' )->header_like( + Location => qr|^\/api\/v1\/tickets/\d*|, + 'SWAGGER3.4.1' + )->json_is( '/biblio_id' => $ticket->{biblio_id} ) + ->json_is( '/title' => $ticket->{title} ) + ->json_is( '/body' => $ticket->{body} ) + ->json_is( '/reporter_id' => $librarian->id )->tx->res->json->{ticket_id}; + + # Authorized attempt to create with null id + $ticket->{ticket_id} = undef; + $t->post_ok( "//$userid:$password@/api/v1/tickets" => json => $ticket ) + ->status_is(400)->json_has('/errors'); + + # Authorized attempt to create with existing id + $ticket->{ticket_id} = $ticket_id; + $t->post_ok( "//$userid:$password@/api/v1/tickets" => json => $ticket ) + ->status_is(400)->json_is( + "/errors" => [ + { + message => "Read-only.", + path => "/body/ticket_id" + } + ] + ); + + # Authorized attempt to write missing data + my $ticket_with_missing_field = { + biblio_id => $biblio->id, + body => "Test ticket details", + }; + + $t->post_ok( "//$userid:$password@/api/v1/tickets" => json => + $ticket_with_missing_field )->status_is(400)->json_is( + "/errors" => [ + { + message => "Missing property.", + path => "/body/title" + } + ] + ); + + $schema->storage->txn_rollback; +}; + +subtest 'update() tests' => sub { + + plan tests => 15; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**9 } # editcatalogue flag = 9 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $ticket_id = $builder->build_object( { class => 'Koha::Tickets' } )->id; + + # Unauthorized attempt to update + $t->put_ok( + "//$unauth_userid:$password@/api/v1/tickets/$ticket_id" => json => + { name => 'New unauthorized name change' } )->status_is(403); + + # Attempt partial update on a PUT + my $ticket_with_missing_field = { + body => "Test ticket details", + }; + + $t->put_ok( "//$userid:$password@/api/v1/tickets/$ticket_id" => json => + $ticket_with_missing_field )->status_is(400) + ->json_is( "/errors" => + [ { message => "Missing property.", path => "/body/title" } ] ); + + # Full object update on PUT + my $ticket_with_updated_field = { + title => "Test ticket update", + body => "Test ticket update details", + }; + + $t->put_ok( "//$userid:$password@/api/v1/tickets/$ticket_id" => json => + $ticket_with_updated_field )->status_is(200) + ->json_is( '/title' => 'Test ticket update' ); + + # Authorized attempt to write invalid data + my $ticket_with_invalid_field = { + blah => "Ticket Blah", + title => "Test ticket update", + body => "Test ticket update details", + }; + + $t->put_ok( "//$userid:$password@/api/v1/tickets/$ticket_id" => json => + $ticket_with_invalid_field )->status_is(400)->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + my $ticket_to_delete = + $builder->build_object( { class => 'Koha::Tickets' } ); + my $non_existent_id = $ticket_to_delete->id; + $ticket_to_delete->delete; + + $t->put_ok( + "//$userid:$password@/api/v1/tickets/$non_existent_id" => json => + $ticket_with_updated_field )->status_is(404); + + # Wrong method (POST) + $ticket_with_updated_field->{ticket_id} = 2; + + $t->post_ok( "//$userid:$password@/api/v1/tickets/$ticket_id" => json => + $ticket_with_updated_field )->status_is(404); + + $schema->storage->txn_rollback; +}; + +subtest 'delete() tests' => sub { + + plan tests => 7; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**9 } # editcatalogue flag = 9 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $ticket_id = $builder->build_object( { class => 'Koha::Tickets' } )->id; + + # Unauthorized attempt to delete + $t->delete_ok("//$unauth_userid:$password@/api/v1/tickets/$ticket_id") + ->status_is(403); + + $t->delete_ok("//$userid:$password@/api/v1/tickets/$ticket_id") + ->status_is( 204, 'SWAGGER3.2.4' )->content_is( '', 'SWAGGER3.3.4' ); + + $t->delete_ok("//$userid:$password@/api/v1/tickets/$ticket_id") + ->status_is(404); + + $schema->storage->txn_rollback; +}; -- 2.39.1