From dc978e1a91c6c94f4ccdc7b605504e0d968afcc6 Mon Sep 17 00:00:00 2001 From: "Michael J. Giarlo" Date: Fri, 29 Sep 2023 13:52:41 -0700 Subject: [PATCH 1/2] Add endpoint for retrieving MARC XML Fixes sul-dlss/dor-services-app#4476 This commit moves the ability to retrieve MARCXML for a given barcode or Folio instance HRID from dor-services-app and -client to the FolioClient. --- lib/folio_client.rb | 18 +- lib/folio_client/data_import.rb | 1 - lib/folio_client/source_storage.rb | 33 ++ spec/folio_client/source_storage_spec.rb | 377 +++++++++++++---------- spec/folio_client_spec.rb | 28 ++ 5 files changed, 292 insertions(+), 165 deletions(-) diff --git a/lib/folio_client.rb b/lib/folio_client.rb index f1dc37d..f1cccf6 100644 --- a/lib/folio_client.rb +++ b/lib/folio_client.rb @@ -3,8 +3,9 @@ require "active_support/core_ext/module/delegation" require "active_support/core_ext/object/blank" require "faraday" -require "singleton" +require "marc" require "ostruct" +require "singleton" require "zeitwerk" # Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes @@ -72,10 +73,10 @@ def configure(url:, login_params:, okapi_headers:, timeout: default_timeout) delegate :config, :connection, :data_import, :default_timeout, :edit_marc_json, :fetch_external_id, :fetch_hrid, :fetch_instance_info, - :fetch_marc_hash, :get, :has_instance_status?, :http_get_headers, - :http_post_and_put_headers, :interface_details, :job_profiles, - :organization_interfaces, :organizations, :post, :put, to: :instance - end + :fetch_marc_hash, :fetch_marc_xml, :get, :has_instance_status?, + :http_get_headers, :http_post_and_put_headers, :interface_details, + :job_profiles, :organization_interfaces, :organizations, :post, :put, to: + :instance end attr_accessor :config @@ -175,6 +176,13 @@ def fetch_marc_hash(...) .fetch_marc_hash(...) end + # @see SourceStorage#fetch_marc_xml + def fetch_marc_xml(...) + SourceStorage + .new(self) + .fetch_marc_xml(...) + end + # @see Inventory#has_instance_status? def has_instance_status?(...) Inventory diff --git a/lib/folio_client/data_import.rb b/lib/folio_client/data_import.rb index 006fca3..f1e95f2 100644 --- a/lib/folio_client/data_import.rb +++ b/lib/folio_client/data_import.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "date" -require "marc" require "stringio" class FolioClient diff --git a/lib/folio_client/source_storage.rb b/lib/folio_client/source_storage.rb index ba6d63e..5b7d88c 100644 --- a/lib/folio_client/source_storage.rb +++ b/lib/folio_client/source_storage.rb @@ -3,6 +3,8 @@ class FolioClient # Lookup records in Folio Source Storage class SourceStorage + FIELDS_TO_REMOVE = %w[001 003].freeze + attr_accessor :client # @param client [FolioClient] the configured client @@ -24,5 +26,36 @@ def fetch_marc_hash(instance_hrid:) response_hash["sourceRecords"].first["parsedRecord"]["content"] end + + # get marc bib data as MARCXML from folio given an instance HRID + # @param instance_hrid [String] the instance HRID to use for MARC lookup + # @param barcode [String] the barcode to use for MARC lookup + # @return [String] MARCXML string + # @raise [ResourceNotFound] + # @raise [MultipleResourcesFound] + def fetch_marc_xml(instance_hrid: nil, barcode: nil) + raise ArgumentError, "Either a barcode or a Folio instance HRID must be provided" if barcode.nil? && instance_hrid.nil? + + instance_hrid ||= client.fetch_hrid(barcode: barcode) + + raise ResourceNotFound, "Catalog record not found. HRID: #{instance_hrid} | Barcode: #{barcode}" if instance_hrid.blank? + + marc_record = MARC::Record.new_from_hash( + fetch_marc_hash(instance_hrid: instance_hrid) + ) + updated_marc = MARC::Record.new + updated_marc.leader = marc_record.leader + marc_record.fields.each do |field| + # explicitly remove all listed tags from the record + next if FIELDS_TO_REMOVE.include?(field.tag) + + updated_marc.fields << field + end + # explicitly inject the instance_hrid into the 001 field + updated_marc.fields << MARC::ControlField.new("001", instance_hrid) + # explicitly inject FOLIO into the 003 field + updated_marc.fields << MARC::ControlField.new("003", "FOLIO") + updated_marc.to_xml.to_s + end end end diff --git a/spec/folio_client/source_storage_spec.rb b/spec/folio_client/source_storage_spec.rb index 0f6a4df..861e1d0 100644 --- a/spec/folio_client/source_storage_spec.rb +++ b/spec/folio_client/source_storage_spec.rb @@ -10,8 +10,7 @@ let(:token) { "aLongSTring.eNCodinga.JwTeeeee" } let(:client) { FolioClient.configure(**args) } let(:instance_hrid) { "a666" } - - let(:search_instance_response) { + let(:search_instance_response) do {"totalRecords" => 1, "instances" => [ {"id" => "some_long_uuid_that_is_long", @@ -20,16 +19,101 @@ "isBoundWith" => false, "holdings" => []} ]} - } - - let(:post_authn_request_headers) { + end + let(:post_authn_request_headers) do { "Accept" => "application/json, text/plain", "Content-Type" => "application/json", "Some-Bogus-Headers" => "here", "X-Okapi-Token" => token } - } + end + let(:source_storage_response) do + {"sourceRecords" => + [{"recordId" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", + "snapshotId" => "5ae00995-bcb3-4fdc-8519-75c1357c44c4", + "recordType" => "MARC_BIB", + "parsedRecord" => + {"id" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", + "content" => + {"fields" => + [ + {"001" => "a666"}, + {"003" => "SIRSI"}, + {"005" => "19900820141050.0"}, + {"008" => "750409s1961||||enk ||| | eng "}, + {"010" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => " 62039356\\\\72b2"}]}}, + {"040" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"d" => "OrLoB"}]}}, + {"050" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "M231.B66 Bb maj. 1961"}]}}, + {"100" => + {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Boccherini, Luigi,"}, {"d" => "1743-1805."}]}}, + {"240" => + {"ind1" => "1", + "ind2" => "0", + "subfields" => [{"a" => "Sonatas,"}, {"m" => "cello, continuo,"}, {"r" => "B♭ major"}]}}, + {"245" => + {"ind1" => " ", + "ind2" => "0", + "subfields" => + [ + {"a" => "Sonata no. 7, in B flat, for violoncello and piano."}, + {"c" => + "Edited with realization of the basso continuo by Fritz Spiegl and Walter Bergamnn. Violoncello part edited by Joan Dickson."} + ]}}, + {"260" => + {"ind1" => " ", + "ind2" => " ", + "subfields" => + [{"a" => "London, Schott; New York, Associated Music Publishers"}, {"c" => "[c1961]"}]}}, + {"300" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "score (20 p.) & part."}, {"c" => "29cm."}]}}, + {"490" => {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Edition [Schott] 10731"}]}}, + {"500" => + {"ind1" => " ", + "ind2" => " ", + "subfields" => + [{"a" => + "Edited from a recently discovered ms. Closely parallels Gruetzmacher's free arrangement of the Violoncello concerto, G. 482."}]}}, + {"596" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "31"}]}}, + {"650" => {"ind1" => " ", "ind2" => "0", "subfields" => [{"a" => "Sonatas (Cello and harpsichord)"}]}}, + {"700" => + {"ind1" => "1", + "ind2" => "2", + "subfields" => + [ + {"a" => "Boccherini, Luigi,"}, + {"d" => "1743-1805."}, + {"t" => "Concertos,"}, + {"m" => "cello, orchestra,"}, + {"n" => "G. 482,"}, + {"r" => "B♭ major"}, + {"o" => "arranged."} + ]}}, + {"830" => {"ind1" => " ", "ind2" => "0", "subfields" => [{"a" => "Edition Schott"}, {"v" => "10731"}]}}, + {"998" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "SCORE"}]}}, + {"035" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "(OCoLC-M)17708345"}]}}, + {"035" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "(OCoLC-I)268876650"}]}}, + {"918" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "666"}]}}, + {"035" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "AAA0675"}]}}, + {"999" => + {"ind1" => "f", + "ind2" => "f", + "subfields" => + [ + {"i" => "696ef04d-1902-5a70-aebf-98d287bce1a1"}, + {"s" => "992460aa-bfe6-50ff-93f6-65c6aa786a43"} + ]}} + ], + "leader" => "01185ccm a2200301 4500"}}, + "deleted" => false, + "externalIdsHolder" => {"instanceId" => "696ef04d-1902-5a70-aebf-98d287bce1a1", "instanceHrid" => "a666"}, + "additionalInfo" => {"suppressDiscovery" => false}, + "metadata" => + {"createdDate" => "2023-02-11T03:54:43.938+00:00", + "createdByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2", + "updatedDate" => "2023-02-11T03:54:44.574+00:00", + "updatedByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2"}}], + "totalRecords" => 1} + end before do # the client is initialized with a fake token (see comment in FolioClient.configure for why). this @@ -46,16 +130,41 @@ .to_return(status: 200, body: source_storage_response.to_json) end - context "when exactly 1 instance record is found" do - let(:source_storage_response) { - {"sourceRecords" => - [{"recordId" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", - "snapshotId" => "5ae00995-bcb3-4fdc-8519-75c1357c44c4", - "recordType" => "MARC_BIB", - "parsedRecord" => - {"id" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", - "content" => - {"fields" => + describe "#fetch_marc_hash" do + context "when exactly 1 instance record is found (happy path)" do + it "returns the parsed JSON as a hash for the one record that was found" do + result = source_storage.fetch_marc_hash(instance_hrid: instance_hrid) + expect(result["fields"].select { |field_hash| field_hash.key?("001") }.map(&:values)).to eq([["a666"]]) + expect(result["fields"].select { |field_hash| field_hash.key?("008") }.map(&:values)).to eq([["750409s1961||||enk ||| | eng "]]) + expect(result["fields"].select { |field_hash| field_hash.key?("050") }.map(&:values)).to eq([[{"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "M231.B66 Bb maj. 1961"}]}]]) + end + end + + context "when no instance records are found" do + let(:source_storage_response) do + {"sourceRecords" => [], "totalRecords" => 0} + end + + it "raises a NotFound exception" do + expect { source_storage.fetch_marc_hash(instance_hrid: instance_hrid) }.to raise_error(FolioClient::ResourceNotFound, "No records found for #{instance_hrid}") + end + end + + context "when multiple instance records are found" do + # based on a real Folio response, but omits some returned fields, and duplicates and lightly + # modifies the one record from the real response. that makes this spec file slightly less gigantic, + # and still simulates the relevant part of the response structure. + let(:source_storage_response) { + {"sourceRecords" => + [ + { + "recordId" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", + "snapshotId" => "5ae00995-bcb3-4fdc-8519-75c1357c44c4", + "recordType" => "MARC_BIB", + "parsedRecord" => + {"id" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", + "content" => + {"fields" => [{"001" => "a666"}, {"003" => "SIRSI"}, {"005" => "19900820141050.0"}, @@ -64,161 +173,111 @@ {"040" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"d" => "OrLoB"}]}}, {"050" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "M231.B66 Bb maj. 1961"}]}}, {"100" => - {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Boccherini, Luigi,"}, {"d" => "1743-1805."}]}}, + {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Boccherini, Luigi,"}, {"d" => "1743-1805."}]}}, {"240" => - {"ind1" => "1", - "ind2" => "0", - "subfields" => [{"a" => "Sonatas,"}, {"m" => "cello, continuo,"}, {"r" => "B♭ major"}]}}, - {"245" => - {"ind1" => " ", - "ind2" => "0", - "subfields" => - [{"a" => "Sonata no. 7, in B flat, for violoncello and piano."}, - {"c" => - "Edited with realization of the basso continuo by Fritz Spiegl and Walter Bergamnn. Violoncello part edited by Joan Dickson."}]}}, - {"260" => - {"ind1" => " ", - "ind2" => " ", - "subfields" => - [{"a" => "London, Schott; New York, Associated Music Publishers"}, {"c" => "[c1961]"}]}}, - {"300" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "score (20 p.) & part."}, {"c" => "29cm."}]}}, - {"490" => {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Edition [Schott] 10731"}]}}, - {"500" => - {"ind1" => " ", - "ind2" => " ", - "subfields" => - [{"a" => - "Edited from a recently discovered ms. Closely parallels Gruetzmacher's free arrangement of the Violoncello concerto, G. 482."}]}}, - {"596" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "31"}]}}, - {"650" => {"ind1" => " ", "ind2" => "0", "subfields" => [{"a" => "Sonatas (Cello and harpsichord)"}]}}, - {"700" => - {"ind1" => "1", - "ind2" => "2", - "subfields" => - [{"a" => "Boccherini, Luigi,"}, - {"d" => "1743-1805."}, - {"t" => "Concertos,"}, - {"m" => "cello, orchestra,"}, - {"n" => "G. 482,"}, - {"r" => "B♭ major"}, - {"o" => "arranged."}]}}, - {"830" => {"ind1" => " ", "ind2" => "0", "subfields" => [{"a" => "Edition Schott"}, {"v" => "10731"}]}}, - {"998" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "SCORE"}]}}, - {"035" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "(OCoLC-M)17708345"}]}}, - {"035" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "(OCoLC-I)268876650"}]}}, - {"918" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "666"}]}}, - {"035" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "AAA0675"}]}}, - {"999" => - {"ind1" => "f", - "ind2" => "f", - "subfields" => - [{"i" => "696ef04d-1902-5a70-aebf-98d287bce1a1"}, - {"s" => "992460aa-bfe6-50ff-93f6-65c6aa786a43"}]}}], - "leader" => "01185ccm a2200301 4500"}}, - "deleted" => false, - "externalIdsHolder" => {"instanceId" => "696ef04d-1902-5a70-aebf-98d287bce1a1", "instanceHrid" => "a666"}, - "additionalInfo" => {"suppressDiscovery" => false}, - "metadata" => - {"createdDate" => "2023-02-11T03:54:43.938+00:00", - "createdByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2", - "updatedDate" => "2023-02-11T03:54:44.574+00:00", - "updatedByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2"}}], - "totalRecords" => 1} - } - - it "returns the parsed JSON as a hash for the one record that was found" do - result = source_storage.fetch_marc_hash(instance_hrid: instance_hrid) - expect(result["fields"].select { |field_hash| field_hash.key?("001") }.map(&:values)).to eq([["a666"]]) - expect(result["fields"].select { |field_hash| field_hash.key?("008") }.map(&:values)).to eq([["750409s1961||||enk ||| | eng "]]) - expect(result["fields"].select { |field_hash| field_hash.key?("050") }.map(&:values)).to eq([[{"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "M231.B66 Bb maj. 1961"}]}]]) - end - end - - context "when no instance records are found" do - let(:source_storage_response) { - {"sourceRecords" => [], "totalRecords" => 0} - } - - it "raises a NotFound exception" do - expect { source_storage.fetch_marc_hash(instance_hrid: instance_hrid) }.to raise_error(FolioClient::ResourceNotFound, "No records found for #{instance_hrid}") - end - end - - context "when multiple instance records are found" do - # based on a real Folio response, but omits some returned fields, and duplicates and lightly - # modifies the one record from the real response. that makes this spec file slightly less gigantic, - # and still simulates the relevant part of the response structure. - let(:source_storage_response) { - {"sourceRecords" => - [ - { - "recordId" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", - "snapshotId" => "5ae00995-bcb3-4fdc-8519-75c1357c44c4", - "recordType" => "MARC_BIB", - "parsedRecord" => - {"id" => "992460aa-bfe6-50ff-93f6-65c6aa786a43", - "content" => - {"fields" => - [{"001" => "a666"}, - {"003" => "SIRSI"}, - {"005" => "19900820141050.0"}, - {"008" => "750409s1961||||enk ||| | eng "}, - {"010" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => " 62039356\\\\72b2"}]}}, - {"040" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"d" => "OrLoB"}]}}, - {"050" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "M231.B66 Bb maj. 1961"}]}}, - {"100" => - {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Boccherini, Luigi,"}, {"d" => "1743-1805."}]}}, - {"240" => - {"ind1" => "1", - "ind2" => "0", - "subfields" => [{"a" => "Sonatas,"}, {"m" => "cello, continuo,"}, {"r" => "B♭ major"}]}}], - "leader" => "01185ccm a2200301 4500"}}, - "deleted" => false, - "externalIdsHolder" => {"instanceId" => "696ef04d-1902-5a70-aebf-98d287bce1a1", "instanceHrid" => "a666"}, - "additionalInfo" => {"suppressDiscovery" => false}, - "metadata" => + {"ind1" => "1", + "ind2" => "0", + "subfields" => [{"a" => "Sonatas,"}, {"m" => "cello, continuo,"}, {"r" => "B♭ major"}]}}], + "leader" => "01185ccm a2200301 4500"}}, + "deleted" => false, + "externalIdsHolder" => {"instanceId" => "696ef04d-1902-5a70-aebf-98d287bce1a1", "instanceHrid" => "a666"}, + "additionalInfo" => {"suppressDiscovery" => false}, + "metadata" => {"createdDate" => "2023-02-11T03:54:43.938+00:00", "createdByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2", "updatedDate" => "2023-02-11T03:54:44.574+00:00", "updatedByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2"} - }, - { - "recordId" => "992460aa-bfe6-50ff-93f6-65c6aa786a54", - "snapshotId" => "5ae00995-bcb3-4fdc-8519-75c1357c44d5", - "recordType" => "MARC_BIB", - "parsedRecord" => + }, + { + "recordId" => "992460aa-bfe6-50ff-93f6-65c6aa786a54", + "snapshotId" => "5ae00995-bcb3-4fdc-8519-75c1357c44d5", + "recordType" => "MARC_BIB", + "parsedRecord" => {"id" => "992460aa-bfe6-50ff-93f6-65c6aa786a54", "content" => - {"fields" => - [{"001" => "a666"}, - {"003" => "SIRSI"}, - {"005" => "19900820141050.0"}, - {"008" => "750409s1961||||enk ||| | eng "}, - {"010" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => " 62039356\\\\72b2"}]}}, - {"040" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"d" => "OrLoB"}]}}, - {"050" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "M231.B66 Bb maj. 1961"}]}}, - {"100" => - {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Boccherini, Luigi,"}, {"d" => "1743-1805."}]}}, - {"240" => - {"ind1" => "1", - "ind2" => "0", - "subfields" => [{"a" => "Sonatas,"}, {"m" => "cello, continuo,"}, {"r" => "B♭ major"}]}}], - "leader" => "01185ccm a2200301 4500"}}, - "deleted" => false, - "externalIdsHolder" => {"instanceId" => "696ef04d-1902-5a70-aebf-98d287bce1b2", "instanceHrid" => "a666"}, - "additionalInfo" => {"suppressDiscovery" => false}, - "metadata" => + {"fields" => + [{"001" => "a666"}, + {"003" => "SIRSI"}, + {"005" => "19900820141050.0"}, + {"008" => "750409s1961||||enk ||| | eng "}, + {"010" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => " 62039356\\\\72b2"}]}}, + {"040" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"d" => "OrLoB"}]}}, + {"050" => {"ind1" => " ", "ind2" => " ", "subfields" => [{"a" => "M231.B66 Bb maj. 1961"}]}}, + {"100" => + {"ind1" => "1", "ind2" => " ", "subfields" => [{"a" => "Boccherini, Luigi,"}, {"d" => "1743-1805."}]}}, + {"240" => + {"ind1" => "1", + "ind2" => "0", + "subfields" => [{"a" => "Sonatas,"}, {"m" => "cello, continuo,"}, {"r" => "B♭ major"}]}}], + "leader" => "01185ccm a2200301 4500"}}, + "deleted" => false, + "externalIdsHolder" => {"instanceId" => "696ef04d-1902-5a70-aebf-98d287bce1b2", "instanceHrid" => "a666"}, + "additionalInfo" => {"suppressDiscovery" => false}, + "metadata" => {"createdDate" => "2023-02-12T03:54:43.938+00:00", "createdByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2", "updatedDate" => "2023-02-12T03:54:44.574+00:00", "updatedByUserId" => "3e2ed889-52f2-45ce-8a30-8767266f07d2"} - } - ], - "totalRecords" => 2} - } + } + ], + "totalRecords" => 2} + } + + it "raises a MultipleRecordsForIdentifier exception" do + expect { source_storage.fetch_marc_hash(instance_hrid: instance_hrid) }.to raise_error(FolioClient::MultipleResourcesFound, "Expected 1 record for #{instance_hrid}, but found 2") + end + end + end + + describe "#fetch_marc_xml" do + context "with neither a barcode nor an instance HRID" do + it "raises ArgumentError" do + expect { source_storage.fetch_marc_xml(barcode: nil, instance_hrid: nil) } + .to raise_error(ArgumentError, /Either a barcode or a Folio instance HRID must be provided/) + end + end + + context "with a barcode that is not found" do + let(:source_storage_response) do + {"sourceRecords" => [], "totalRecords" => 0} + end + + before do + allow(source_storage.client).to receive(:fetch_hrid).and_return(nil) + end + + it "raises ResourceNotFound" do + expect { source_storage.fetch_marc_xml(barcode: "12345678", instance_hrid: nil) } + .to raise_error(FolioClient::ResourceNotFound, /Catalog record not found/) + end + end + + context "with an instance HRID that is not found" do + let(:source_storage_response) do + {"sourceRecords" => [], "totalRecords" => 0} + end + + it "raises ResourceNotFound" do + expect { source_storage.fetch_marc_xml(barcode: nil, instance_hrid: instance_hrid) } + .to raise_error(FolioClient::ResourceNotFound, /No records found for #{instance_hrid}/) + end + end + + context "with an instance HRID that is found, whether given a barcode or not (happy path)" do + it "uses the HRID and returns MARC XML" do + expected = <<~MARCXML.chomp + 01185ccm a2200301 450019900820141050.0750409s1961||||enk ||| | eng 62039356\\\\72b2OrLoBM231.B66 Bb maj. 1961Boccherini, Luigi,1743-1805.Sonatas,cello, continuo,B♭ majorSonata no. 7, in B flat, for violoncello and piano.Edited with realization of the basso continuo by Fritz Spiegl and Walter Bergamnn. Violoncello part edited by Joan Dickson.London, Schott; New York, Associated Music Publishers[c1961]score (20 p.) & part.29cm.Edition [Schott] 10731Edited from a recently discovered ms. Closely parallels Gruetzmacher's free arrangement of the Violoncello concerto, G. 482.31Sonatas (Cello and harpsichord)Boccherini, Luigi,1743-1805.Concertos,cello, orchestra,G. 482,B♭ majorarranged.Edition Schott10731SCORE(OCoLC-M)17708345(OCoLC-I)268876650666AAA0675696ef04d-1902-5a70-aebf-98d287bce1a1992460aa-bfe6-50ff-93f6-65c6aa786a43a666FOLIO + MARCXML + expect(source_storage.fetch_marc_xml(barcode: nil, instance_hrid: instance_hrid)).to eq(expected) + end + + it "ensures returned MARC XML has expected 001 field" do + expect(source_storage.fetch_marc_xml(barcode: nil, instance_hrid: instance_hrid)).to match("a666") + end - it "raises a MultipleRecordsForIdentifier exception" do - expect { source_storage.fetch_marc_hash(instance_hrid: instance_hrid) }.to raise_error(FolioClient::MultipleResourcesFound, "Expected 1 record for #{instance_hrid}, but found 2") + it "ensures returned MARC XML has expected 003 field" do + expect(source_storage.fetch_marc_xml(barcode: nil, instance_hrid: instance_hrid)).to match("FOLIO") + end end end end diff --git a/spec/folio_client_spec.rb b/spec/folio_client_spec.rb index 4f33d9d..b366825 100644 --- a/spec/folio_client_spec.rb +++ b/spec/folio_client_spec.rb @@ -324,6 +324,34 @@ end end + describe ".fetch_marc_xml" do + let(:instance_hrid) { "a12854819" } + + before do + allow(described_class.instance).to receive(:fetch_marc_xml).with(instance_hrid: instance_hrid) + end + + it "invokes instance#fetch_marc_xml" do + client.fetch_marc_xml(instance_hrid: instance_hrid) + expect(client.instance).to have_received(:fetch_marc_xml).with(instance_hrid: instance_hrid) + end + end + + describe "#fetch_marc_xml" do + let(:instance_hrid) { "123456" } + let(:source_storage) { instance_double(described_class::SourceStorage) } + + before do + allow(described_class::SourceStorage).to receive(:new).and_return(source_storage) + allow(source_storage).to receive(:fetch_marc_xml) + end + + it "invokes SourceStorage#fetch_marc_xml" do + client.fetch_marc_xml(instance_hrid: instance_hrid) + expect(source_storage).to have_received(:fetch_marc_xml).once + end + end + describe ".data_import" do let(:job_profile_id) { "4ba4f4ab" } let(:job_profile_name) { "ETDs" } From 6112115dbab76734ae2c71d1f6d08b4b89bc6022 Mon Sep 17 00:00:00 2001 From: "Michael J. Giarlo" Date: Fri, 29 Sep 2023 13:52:58 -0700 Subject: [PATCH 2/2] Add an integration-testing script (per GlobusClient approach) --- README.md | 16 ++++++++++++++++ api_test.rb | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100755 api_test.rb diff --git a/README.md b/README.md index f3b9ce9..3789234 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,22 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). +## Integration Testing + +To test that the gem works against the Folio APIs, run `api_test.rb` via: + +```shell +# NOTE: This is bash syntax, YMMV +$ export OKAPI_PASSWORD=$(vault kv get --field=content puppet/application/folio/stage/app_sdr_password) +$ export OKAPI_TENANT=sul +$ export OKAPI_USER=app_sdr +$ export OKAPI_URL=https://okapi-stage.stanford.edu +# NOTE: The args below are a list of MARC files +$ bundle exec ruby ./api_test.rb /path/to/marc/files/test.mrc /another/marc/file/at/foobar.marc +``` + +Inspect the output and make sure there are no errors. + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/sul-dlss/folio_client. diff --git a/api_test.rb b/api_test.rb new file mode 100755 index 0000000..836011c --- /dev/null +++ b/api_test.rb @@ -0,0 +1,38 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "folio_client" + +marc_files = *ARGV + +client = + FolioClient.configure( + url: ENV["OKAPI_URL"], + login_params: { + username: ENV["OKAPI_USER"], + password: ENV["OKAPI_PASSWORD"] + }, + okapi_headers: { + "X-Okapi-Tenant": ENV["OKAPI_TENANT"], + "User-Agent": "folio_client gem (testing)" + } + ) + +pp(client.fetch_marc_hash(instance_hrid: "a666")) + +puts client.fetch_marc_xml(instance_hrid: "a666") +puts client.fetch_marc_xml(barcode: "20503330279") + +records = marc_files.flat_map do |marc_file_path| + MARC::Reader.new(marc_file_path).to_a +end + +data_importer = + client.data_import( + records: records, + job_profile_id: "e34d7b92-9b83-11eb-a8b3-0242ac130003", + job_profile_name: "Default - Create instance and SRS MARC Bib" + ) + +puts data_importer.wait_until_complete +puts data_importer.instance_hrids