From 28ac588a8dbe3f07fb09e6f7421ab386d62233db Mon Sep 17 00:00:00 2001
From: Agustin Moyano <agustinmoyano@theke.io>
Date: Thu, 6 Apr 2023 15:55:26 -0300
Subject: [PATCH] Bug 31795: Add REST endpoint to add an authority

To test:
1. Apply patch
2. Set RESTBasicAuth preference to true
3. Make a POST request to /api/v1/authorities with one of the following content type header
  - application/json
  - application/marcxml+xml
  - application/marc-in-json
  - application/marc
4. If content type is other than application/json, then add the following header to the request: "x-authority-type: <auth type code>"
5. Check that the authority is created
6. Sign off

Signed-off-by: David Nind <david@davidnind.com>
---
 Koha/REST/V1/Authorities.pm           | 70 ++++++++++++++++++++++++++-
 api/v1/swagger/paths/authorities.yaml | 56 +++++++++++++++++++++
 api/v1/swagger/swagger.yaml           |  8 +++
 t/db_dependent/api/v1/authorities.t   | 70 ++++++++++++++++++++++++++-
 4 files changed, 202 insertions(+), 2 deletions(-)

diff --git a/Koha/REST/V1/Authorities.pm b/Koha/REST/V1/Authorities.pm
index 2164b779014..eac54c3df5b 100644
--- a/Koha/REST/V1/Authorities.pm
+++ b/Koha/REST/V1/Authorities.pm
@@ -20,7 +20,7 @@ use Modern::Perl;
 use Mojo::Base 'Mojolicious::Controller';
 
 use Koha::Authorities;
-use C4::AuthoritiesMarc qw( DelAuthority );
+use C4::AuthoritiesMarc qw( DelAuthority AddAuthority FindDuplicateAuthority);
 
 use List::MoreUtils qw( any );
 use MARC::Record::MiJ;
@@ -128,4 +128,72 @@ sub delete {
     };
 }
 
+=head3 add
+
+Controller function that handles creating an authority object
+
+=cut
+
+sub add {
+    my $c = shift->openapi->valid_input or return;
+
+    try {
+        my $body = $c->validation->param('Body');
+
+        my $flavour =
+            C4::Context->preference('marcflavour') eq 'UNIMARC'
+            ? 'UNIMARCAUTH'
+            : 'MARC21';
+
+        my $record;
+        my $authtypecode;
+
+        if ( $c->req->headers->content_type =~ m/application\/json/ ) {
+            $record = MARC::Record->new_from_xml( $body->{marcxml}, 'UTF-8', $flavour );
+            $authtypecode = $body->{authtypecode};
+        } else {
+            $authtypecode = $c->validation->param('x-authority-type');
+            if ( $c->req->headers->content_type =~ m/application\/marcxml\+xml/ ) {
+                $record = MARC::Record->new_from_xml( $body, 'UTF-8', $flavour );
+            } elsif ( $c->req->headers->content_type =~ m/application\/marc-in-json/ ) {
+                $record = MARC::Record->new_from_mij_structure( $body );
+            } elsif ( $c->req->headers->content_type =~ m/application\/marc/ ) {
+                $record = MARC::Record->new_from_usmarc( $body );
+            } else {
+                return $c->render(
+                    status  => 406,
+                    openapi => [
+                        "application/json",
+                        "application/marcxml+xml",
+                        "application/marc-in-json",
+                        "application/marc"
+                    ]
+                );
+            }
+        }
+
+        my ($duplicateauthid,$duplicateauthvalue);
+            ($duplicateauthid,$duplicateauthvalue) = FindDuplicateAuthority($record,$authtypecode);
+
+        my $confirm_not_duplicate = $c->validation->param('x-confirm-not-duplicate');
+
+        return $c->render(
+            status  => 400,
+            openapi => {
+                error => "Duplicate authority $duplicateauthid"
+            }
+        ) unless !$duplicateauthid || $confirm_not_duplicate;
+
+        my $authid = AddAuthority( $record, undef, $authtypecode );
+
+        $c->render(
+            status  => 200,
+            openapi => { id => $authid }
+        );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+}
+
 1;
diff --git a/api/v1/swagger/paths/authorities.yaml b/api/v1/swagger/paths/authorities.yaml
index 80c85d1a04b..a6575ce8095 100644
--- a/api/v1/swagger/paths/authorities.yaml
+++ b/api/v1/swagger/paths/authorities.yaml
@@ -1,4 +1,60 @@
 ---
+"/authorities":
+  post:
+    x-mojo-to: Authorities#add
+    operationId: addAuthority
+    tags:
+      - authorities
+    summary: Add authority
+    parameters:
+      - name: Body
+        in: body
+        description: A JSON object or the Marc string describing an authority
+        required: true
+        schema:
+          type:
+            - string
+            - object
+      - $ref: "../swagger.yaml#/parameters/authority_type_header"
+      - $ref: "../swagger.yaml#/parameters/confirm_not_duplicate_header"
+    produces:
+      - application/json
+    responses:
+      "200":
+        description: An authority
+      "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"
+      "406":
+        description: Not acceptable
+        schema:
+          type: array
+          description: Accepted content-types
+          items:
+            type: string
+      "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:
+        editcatalogue: edit_catalogue
 "/authorities/{authority_id}":
   get:
     x-mojo-to: Authorities#get
diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml
index 7d5e04f62d8..43014b64357 100644
--- a/api/v1/swagger/swagger.yaml
+++ b/api/v1/swagger/swagger.yaml
@@ -151,6 +151,8 @@ paths:
     $ref: paths/authorised_value_categories.yaml#/~1authorised_value_categories
   "/authorised_value_categories/{authorised_value_category_name}/authorised_values":
     $ref: "./paths/authorised_values.yaml#/~1authorised_value_categories~1{authorised_value_category_name}~1authorised_values"
+  "/authorities":
+    $ref: paths/authorities.yaml#/~1authorities
   "/authorities/{authority_id}":
     $ref: paths/authorities.yaml#/~1authorities~1{authority_id}
   "/biblios":
@@ -376,6 +378,12 @@ parameters:
     name: authority_id
     required: true
     type: integer
+  authority_type_header:
+    description: Authority type code. Use when content type is not application/json
+    name: x-authority-type
+    in: header
+    required: false
+    type: string
   framework_id_header:
     description: Framework id. Use when content type is not application/json
     name: x-framework-id
diff --git a/t/db_dependent/api/v1/authorities.t b/t/db_dependent/api/v1/authorities.t
index 0f20376c4cb..938fbfd214b 100755
--- a/t/db_dependent/api/v1/authorities.t
+++ b/t/db_dependent/api/v1/authorities.t
@@ -20,7 +20,7 @@ use Modern::Perl;
 use utf8;
 use Encode;
 
-use Test::More tests => 2;
+use Test::More tests => 3;
 use Test::MockModule;
 use Test::Mojo;
 use Test::Warn;
@@ -152,3 +152,71 @@ subtest 'delete() tests' => sub {
 
     $schema->storage->txn_rollback;
 };
+
+subtest 'post() tests' => sub {
+
+    plan tests => 14;
+
+    $schema->storage->txn_begin;
+
+    Koha::Authorities->delete;
+
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 0 } # no permissions
+        }
+    );
+    my $password = 'thePassword123';
+    $patron->set_password( { password => $password, skip_validation => 1 } );
+    my $userid = $patron->userid;
+
+    my $marcxml = q|<?xml version="1.0" encoding="UTF-8"?>
+<record xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.loc.gov/MARC21/slim" xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd">
+    <controlfield tag="001">1001</controlfield>
+    <datafield tag="110" ind1=" " ind2=" ">
+        <subfield code="9">102</subfield>
+        <subfield code="a">My Corporation</subfield>
+    </datafield>
+</record>|;
+
+    my $mij = '{"fields":[{"001":"1001"},{"110":{"subfields":[{"9":"102"},{"a":"My Corporation"}],"ind1":" ","ind2":" "}}],"leader":"                        "}';
+    my $marc = '00079     2200049   45000010005000001100024000051001  9102aMy Corporation';
+    my $json = {
+      authtypecode => "CORPO_NAME",
+      marcxml      => $marcxml
+    };
+
+    $t->post_ok("//$userid:$password@/api/v1/authorities")
+      ->status_is(403, 'Not enough permissions makes it return the right code');
+
+    # Add permissions
+    $builder->build(
+        {
+            source => 'UserPermission',
+            value  => {
+                borrowernumber => $patron->borrowernumber,
+                module_bit     => 9,
+                code           => 'edit_catalogue'
+            }
+        }
+    );
+
+    $t->post_ok("//$userid:$password@/api/v1/authorities" => json => $json)
+      ->status_is(200)
+      ->json_has('/id');
+
+    $t->post_ok("//$userid:$password@/api/v1/authorities" => {'Content-Type' => 'application/marcxml+xml', 'x-authority-type' => 'CORPO_NAME'} => $marcxml)
+      ->status_is(200)
+      ->json_has('/id');
+
+    $t->post_ok("//$userid:$password@/api/v1/authorities" => {'Content-Type' => 'application/marc-in-json', 'x-authority-type' => 'CORPO_NAME'} => $mij)
+      ->status_is(200)
+      ->json_has('/id');
+
+    $t->post_ok("//$userid:$password@/api/v1/authorities" => {'Content-Type' => 'application/marc', 'x-authority-type' => 'CORPO_NAME'} => $marc)
+      ->status_is(200)
+      ->json_has('/id');
+
+    $schema->storage->txn_rollback;
+};
-- 
2.34.1