From ddabb1b04358df2d78a412413093d674240c1115 Mon Sep 17 00:00:00 2001 From: Matthew Bernhardt Date: Fri, 5 Jun 2026 15:27:31 -0400 Subject: [PATCH 1/4] Implement Alma SRU model for availability ** Why are these changes being introduced: For performance reasons, we stripped out holdings information from our initial Primo API call. However, we do want to display these holdings to users where they are available - so we need to add them back as an async API lookup. ** Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/use-598 ** How does this address that need: This adds an AlmaSru model that will handle these async lookups. This ticket calls for only the model, the rest of the integration will happen in subsequent tickets. The model provides a .lookup method which accepts an identifier argument. It validates that identifier, submits it to the Alma SRU endpoint, receives the result, and parses that response into a human-friendly format. ** Document any side effects to this change: We consult a variety of external APIs in this application, and each of them follows slightly different patterns. Hopefully this does not exacerbate that range terribly. --- .env.test | 2 + app/models/alma_sru.rb | 141 ++++++ test/fixtures/alma/sru_nocontrol.xml | 339 +++++++++++++++ test/fixtures/alma/sru_success.xml | 340 +++++++++++++++ test/fixtures/alma/sru_wrong_order.xml | 52 +++ test/models/alma_sru_test.rb | 229 ++++++++++ .../alma_sru_multiple_records.yml | 407 ++++++++++++++++++ .../alma_sru_no_availability.yml | 286 ++++++++++++ .../alma_sru_nonexistent_record.yml | 74 ++++ test/vcr_cassettes/alma_sru_single_record.yml | 296 +++++++++++++ 10 files changed, 2166 insertions(+) create mode 100644 app/models/alma_sru.rb create mode 100644 test/fixtures/alma/sru_nocontrol.xml create mode 100644 test/fixtures/alma/sru_success.xml create mode 100644 test/fixtures/alma/sru_wrong_order.xml create mode 100644 test/models/alma_sru_test.rb create mode 100644 test/vcr_cassettes/alma_sru_multiple_records.yml create mode 100644 test/vcr_cassettes/alma_sru_no_availability.yml create mode 100644 test/vcr_cassettes/alma_sru_nonexistent_record.yml create mode 100644 test/vcr_cassettes/alma_sru_single_record.yml diff --git a/.env.test b/.env.test index 36de855b..ec37bdcf 100644 --- a/.env.test +++ b/.env.test @@ -1,9 +1,11 @@ ALMA_OPENURL=https://na06.alma.exlibrisgroup.com/view/uresolver/01MIT_INST/openurl? +EXL_INST_ID=01MIT_INST TURNSTILE_SITEKEY=test-sitekey TURNSTILE_SECRET=test-secret FEATURE_TIMDEX_FULLTEXT=true FEATURE_GEODATA=false FEATURE_PRIMO_NDE_LINKS=false +MIT_ALMA_URL=https://mit.alma.exlibrisgroup.com MIT_PRIMO_URL=https://mit.primo.exlibrisgroup.com OPENALEX_EMAIL=FAKE_OPENALEX_EMAIL PRIMO_API_KEY=FAKE_PRIMO_API_KEY diff --git a/app/models/alma_sru.rb b/app/models/alma_sru.rb new file mode 100644 index 00000000..d0031464 --- /dev/null +++ b/app/models/alma_sru.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +# Queries the Alma SRU endpoint for holdings data +# +# @reference https://developers.exlibrisgroup.com/alma/integrations/SRU/ +class AlmaSru + class LookupFailure < StandardError; end + + class InvalidAlmaId < StandardError; end + + NAMESPACE = { 'holding' => 'http://www.loc.gov/MARC21/slim' }.freeze + + LOCATION_ORDER = { + 'Hayden Library' => 0, + 'Lewis Music Library' => 1, + 'Rotch Library' => 2, + 'Barker Library' => 3, + 'Dewey Library' => 4 + }.freeze + + # lookup is the primary method of interacting with this model. + # + # It will receive an Alma ID, validate it, look it up in the Alma SRU, and return a formatted result. + # + # It accepts an "alma_client" argument for use when testing, but this is not used in normal operations. + def self.lookup(raw_identifier, alma_client: nil) + return [] unless alma_base_url + + # Validate the raw identifier received. This will raise an InvalidAlmaId if validation fails. + identifier = validate_alma_id(raw_identifier) + + # Build URL + url = alma_sru_url(identifier) + + # Retrieve that URL + alma_http = setup(url, alma_client) + + parse_response(alma_http.timeout(6).get(url), identifier) + rescue InvalidAlmaId + Rails.logger.debug("Invalid Alma ID: #{raw_identifier}") + + [] + rescue LookupFailure => e + Rails.logger.debug("Alma lookup failure: #{e}") + + [] + rescue HTTP::Error + Sentry.capture_message('Alma SRU connection failure') + Rails.logger.error('Alma SRU connection error') + + [] + end + + # parse_response receives the raw response from the Alma SRU endpoint. + # + # For any non-200 response, it raises a LookupFailure. + # + # Other responses (in XML format) are parsed by Nokogiri, and we pluck content with an `AVA` tag. + def self.parse_response(raw_response, reference_identifier) + raise LookupFailure, raw_response.status unless raw_response.status == 200 + + parsed = Nokogiri::XML(raw_response.body.to_s) + + # Confirm that control field 001 matches the identifier we received. + parsed_controlfield = fetch_controlfield(parsed) + raise LookupFailure, 'Control field mismatch' unless parsed_controlfield == reference_identifier + + # Look up all AVA tags + parsed_availabilities = fetch_availabilities(parsed) + + parsed_availabilities.map(&method(:format_availability)) + end + + # validate_alma_id ensures we are only submitting valid Alma IDs to the SRU endpoint. + # + # It needs to do two thigns: + # 1. Remove the "alma" prefix if one is present. Otherwise, no manipulation of the submitted value should occur. + # 2. Enforce the formatting requirements for a valid alma identifier (start with "99", and end with "6761"). + def self.validate_alma_id(raw) + parsed = if raw.start_with?('alma') + raw.delete_prefix('alma') + else + raw + end + + raise InvalidAlmaId unless parsed.to_s.start_with?('99') + raise InvalidAlmaId unless parsed.to_s.end_with?('6761') + + parsed + end + + # ava_to_hash takes an XML element that represents a single availability record + # and converts it to a hash. Each code is a key, while its text is the value. + def self.ava_to_hash(node) + rebuilt = {} + + node.children.each do |child| + rebuilt[child.attribute_nodes[0].value] = child.text if child.instance_of?(Nokogiri::XML::Element) + end + + rebuilt + end + + # fetch_availabilities receives a parsed XML document (Nokogiri::XML::Document) + # + # This document is parsed using xpath to select on the nodes with an tag of AVA, + # and these are then sorted based on a preferred library order. + def self.fetch_availabilities(parsed_xml) + ava_list = parsed_xml.xpath("//holding:datafield[@tag='AVA']", NAMESPACE) + + ava_list + .map { |el| ava_to_hash(el) } + .sort_by { |el| [LOCATION_ORDER.fetch(el['q'], 999), el['q'].to_s] } + end + + # fetch_controlfield receives a parsed XML document (Nokogiri::XML::Document) + # and returns the controlfield with an 001 tag, if one exists. + def self.fetch_controlfield(parsed_xml) + parsed_xml.xpath("//holding:controlfield[@tag='001']", NAMESPACE)&.text + end + + # format_availability receives a hash representing a single availability + # statement, and formats it for human readability. + def self.format_availability(availability) + "#{availability['e']&.humanize} at #{availability['q']} #{availability['c']} (#{availability['d']})" + end + + def self.alma_base_url + ENV.fetch('MIT_ALMA_URL', nil) + end + + def self.alma_sru_url(identifier) + # example identifier: 990000959610106761 + "#{alma_base_url}/view/sru/#{ENV.fetch('EXL_INST_ID')}?version=1.2&operation=searchRetrieve&recordSchema=marcxml" \ + "&query=alma.all_for_ui=#{identifier}" + end + + def self.setup(url, alma_client) + alma_client || HTTP.persistent(url) + end +end diff --git a/test/fixtures/alma/sru_nocontrol.xml b/test/fixtures/alma/sru_nocontrol.xml new file mode 100644 index 00000000..5b363853 --- /dev/null +++ b/test/fixtures/alma/sru_nocontrol.xml @@ -0,0 +1,339 @@ + + 1.2 + 1 + + + marcxml + xml + + + 03917cas 2200889 a 4500 + 20241127113448.0 + 741224c19599999nyuqr1p 0 a0eng^^ + + 61019573 //r892 + + + 0022-1481 + + + JHTRAO + + + 280380 + USPS + + + (MCM)000293592 + + + (MCM)000293592MIT01 + + + (OCoLC)01782922 + + + (OCoLC)01713702 + + + (OCoLC)09764105 + + + (VERA)3557 + + + ASME, United Engineering Center, 345 E. 47th St., New York, NY 10017 + + + DLC + DLC + NSD + DLC + NSD + SER + OCL + AIP + OCL + NSD + OCL + NST + DLC + MYG + + + lc + nsdp + + + MYGG + + + TA1 + .J64 + + + J. heat transfer + + + Journal of heat transfer + + + Journal of heat transfer. + + + New York, N.Y. : + American Society of Mechanical Engineers, + c1959- + + + v. : + ill. ; + 29 cm. + + + Quarterly + + + text + txt + rdacontent + + + unmediated + n + rdamedia + + + volume + nc + rdacarrier + + + Vol. 81, no. 1 (Feb. 1959)- + + + Transactions of the ASME ; + ser. C + + + Title from cover. + + + Applied science & technology index + 0003-6986 + + + Engineering index monthly (1984) + 0742-1974 + + + Engineering index bioengineering abstracts + 0736-6213 + + + Engineering index energy abstracts + 0093-8408 + + + Energy information abstracts + 0147-6521 + + + Environment abstracts + 0093-3287 + + + FLUIDEX + 1978- + + + Abstract bulletin of the Institute of Paper Chemistry + 0020-3033 + + + Computer & control abstracts + 0036-8113 + 1968- + + + Electrical & electronics abstracts + 0036-8105 + 1968- + + + Physics abstracts. Science abstracts. Series A + 0036-8091 + 1968- + + + Chemical abstracts + 0009-2258 + + + SPIN + 1977- + + + Coal abstracts + 0309-4979 + + + Energy research abstracts + 0160-3604 + + + International aerospace abstracts + 0020-5842 + + + Nuclear science abstracts + 0029-5612 + + + Indexes to publications - American Society of Mechanical Engineers + 0569-8227 + + + Vols. for 1978- lack series numeration. + + + Beginning 2000, full-text articles also available via the World Wide Web by subscription, in HTML, PDF, and PostScript formats. Tables of contents for 1996-1999 also available in HTML. + + + Also included in: American Society of Mechanical Engineers. Transactions of the American Society of Mechanical Engineers, 1959-<1967> + + + Heat + Transmission + Periodicals. + + + Heat + Transmission. + fast + + + Periodicals. + lcgft + + + Periodicals. + fast + + + Transactions of the ASME + (OCoLC)19731523 + (DLC)sc 89034060 + + + American Society of Mechanical Engineers. + Transactions of the American Society of Mechanical Engineers + (DLC) 02002053 + (OCoLC)1480830 + + + Journal of heat transfer [fiche] + E36211 960018634 + + + American Society of Mechanical Engineers. + + + Transactions of the ASME (1959) ; + ser. C. + + + jle001205/1 + + + MARCIVEAUT + + + depgy-nooclc210609 + 210609 + depgy-nooclc + + + MARCIVEAUT221103 + + + MARCIVEAUT231208 + + + MARCIVEAUT241112 + + + Vol. 102, no. 4 (Nov. 1980) LIC + + + unpiggy-tang + + + LSA + JRNAL + No Call # + 8 + 57 + All other volumes - use buttons to right + Please check availability at top of page to verify the Annex owns the volume/year you want. + ISSUE + + + New York, N.Y. : American Society of Mechanical Engineers, + 1959 + nyu + + + TA1 + JHTRAO + 01782922 + 01713702 + 09764105 + 61019573 //r892 + 0022-1481 + + + 02 + MYG + + + 990002935920106761 + 22477267920006761 + 01MIT_INST + LSA + Journal Collection (LSA4) + TA.J86.H437 + available + 63 + 0 + LSA4 + Hfcl + 1 + Library Storage Annex + v.81 (1959)-v.131:no.1-6 (2009) + + + 990002935920106761 + 22477267070006761 + 01MIT_INST + ENG + Staff Retrieval - request required + FICHE No Call # + check_holdings + MFORM + 8 + 2 + Barker Library + v.92 (1970)-v.95 (1973),v.97 (1975)-v.119 (1997) + + + + 990002935920106761 + 1 + + + + true + 2026-06-05T14:51:17-0400 + + \ No newline at end of file diff --git a/test/fixtures/alma/sru_success.xml b/test/fixtures/alma/sru_success.xml new file mode 100644 index 00000000..1b0f90ca --- /dev/null +++ b/test/fixtures/alma/sru_success.xml @@ -0,0 +1,340 @@ + + 1.2 + 1 + + + marcxml + xml + + + 03917cas 2200889 a 4500 + 990002935920106761 + 20241127113448.0 + 741224c19599999nyuqr1p 0 a0eng^^ + + 61019573 //r892 + + + 0022-1481 + + + JHTRAO + + + 280380 + USPS + + + (MCM)000293592 + + + (MCM)000293592MIT01 + + + (OCoLC)01782922 + + + (OCoLC)01713702 + + + (OCoLC)09764105 + + + (VERA)3557 + + + ASME, United Engineering Center, 345 E. 47th St., New York, NY 10017 + + + DLC + DLC + NSD + DLC + NSD + SER + OCL + AIP + OCL + NSD + OCL + NST + DLC + MYG + + + lc + nsdp + + + MYGG + + + TA1 + .J64 + + + J. heat transfer + + + Journal of heat transfer + + + Journal of heat transfer. + + + New York, N.Y. : + American Society of Mechanical Engineers, + c1959- + + + v. : + ill. ; + 29 cm. + + + Quarterly + + + text + txt + rdacontent + + + unmediated + n + rdamedia + + + volume + nc + rdacarrier + + + Vol. 81, no. 1 (Feb. 1959)- + + + Transactions of the ASME ; + ser. C + + + Title from cover. + + + Applied science & technology index + 0003-6986 + + + Engineering index monthly (1984) + 0742-1974 + + + Engineering index bioengineering abstracts + 0736-6213 + + + Engineering index energy abstracts + 0093-8408 + + + Energy information abstracts + 0147-6521 + + + Environment abstracts + 0093-3287 + + + FLUIDEX + 1978- + + + Abstract bulletin of the Institute of Paper Chemistry + 0020-3033 + + + Computer & control abstracts + 0036-8113 + 1968- + + + Electrical & electronics abstracts + 0036-8105 + 1968- + + + Physics abstracts. Science abstracts. Series A + 0036-8091 + 1968- + + + Chemical abstracts + 0009-2258 + + + SPIN + 1977- + + + Coal abstracts + 0309-4979 + + + Energy research abstracts + 0160-3604 + + + International aerospace abstracts + 0020-5842 + + + Nuclear science abstracts + 0029-5612 + + + Indexes to publications - American Society of Mechanical Engineers + 0569-8227 + + + Vols. for 1978- lack series numeration. + + + Beginning 2000, full-text articles also available via the World Wide Web by subscription, in HTML, PDF, and PostScript formats. Tables of contents for 1996-1999 also available in HTML. + + + Also included in: American Society of Mechanical Engineers. Transactions of the American Society of Mechanical Engineers, 1959-<1967> + + + Heat + Transmission + Periodicals. + + + Heat + Transmission. + fast + + + Periodicals. + lcgft + + + Periodicals. + fast + + + Transactions of the ASME + (OCoLC)19731523 + (DLC)sc 89034060 + + + American Society of Mechanical Engineers. + Transactions of the American Society of Mechanical Engineers + (DLC) 02002053 + (OCoLC)1480830 + + + Journal of heat transfer [fiche] + E36211 960018634 + + + American Society of Mechanical Engineers. + + + Transactions of the ASME (1959) ; + ser. C. + + + jle001205/1 + + + MARCIVEAUT + + + depgy-nooclc210609 + 210609 + depgy-nooclc + + + MARCIVEAUT221103 + + + MARCIVEAUT231208 + + + MARCIVEAUT241112 + + + Vol. 102, no. 4 (Nov. 1980) LIC + + + unpiggy-tang + + + LSA + JRNAL + No Call # + 8 + 57 + All other volumes - use buttons to right + Please check availability at top of page to verify the Annex owns the volume/year you want. + ISSUE + + + New York, N.Y. : American Society of Mechanical Engineers, + 1959 + nyu + + + TA1 + JHTRAO + 01782922 + 01713702 + 09764105 + 61019573 //r892 + 0022-1481 + + + 02 + MYG + + + 990002935920106761 + 22477267920006761 + 01MIT_INST + LSA + Journal Collection (LSA4) + TA.J86.H437 + available + 63 + 0 + LSA4 + Hfcl + 1 + Library Storage Annex + v.81 (1959)-v.131:no.1-6 (2009) + + + 990002935920106761 + 22477267070006761 + 01MIT_INST + ENG + Staff Retrieval - request required + FICHE No Call # + check_holdings + MFORM + 8 + 2 + Barker Library + v.92 (1970)-v.95 (1973),v.97 (1975)-v.119 (1997) + + + + 990002935920106761 + 1 + + + + true + 2026-06-05T14:51:17-0400 + + \ No newline at end of file diff --git a/test/fixtures/alma/sru_wrong_order.xml b/test/fixtures/alma/sru_wrong_order.xml new file mode 100644 index 00000000..0346adab --- /dev/null +++ b/test/fixtures/alma/sru_wrong_order.xml @@ -0,0 +1,52 @@ + + 1.2 + 1 + + + marcxml + xml + + + 03917cas 2200889 a 4500 + 990002935920106761 + + 990002935920106761 + 22477267920006761 + 01MIT_INST + LSA + Journal Collection (LSA4) + TA.J86.H437 + available + 63 + 0 + LSA4 + Hfcl + 1 + Library Storage Annex + v.81 (1959)-v.131:no.1-6 (2009) + + + 990002935920106761 + 22477267070006761 + 01MIT_INST + ENG + Staff Retrieval - request required + FICHE No Call # + check_holdings + MFORM + 8 + 2 + Barker Library + v.92 (1970)-v.95 (1973),v.97 (1975)-v.119 (1997) + + + + 990002935920106761 + 1 + + + + true + 2026-06-05T14:51:17-0400 + + \ No newline at end of file diff --git a/test/models/alma_sru_test.rb b/test/models/alma_sru_test.rb new file mode 100644 index 00000000..1c912eea --- /dev/null +++ b/test/models/alma_sru_test.rb @@ -0,0 +1,229 @@ +require 'test_helper' + +class AlmaSruMockResponse + attr_reader :status + + def initialize(status, body) + @status = status + @body = body + end + + def to_s + @body + end +end + +class AlmaConnectionError + def timeout(_) + self + end + + def get(_url) + raise HTTP::ConnectionError, 'forced connection failure' + end +end + +class AlmaErrorResponse + def timeout(_) + self + end + + def get(_url) + AlmaSruMockResponse.new(500, 'internal server error') + end +end + +class AlmaSruTest < ActiveSupport::TestCase + test 'lookup returns text for successful lookup' do + VCR.use_cassette('alma sru single record') do + needle = 'alma990014651640106761' + + result = AlmaSru.lookup(needle) + + assert_equal(result, ['Available at Rotch Library Stacks (NA680.C25 2007)']) + end + end + + test 'lookup returns self-service locations first if multiples exist' do + VCR.use_cassette('alma sru multiple records') do + needle = '990002935920106761' + + result = AlmaSru.lookup(needle) + + assert_equal(result[0], 'Check holdings at Barker Library Staff Retrieval - request required (FICHE No Call #)') + assert_equal(result[1], 'Available at Library Storage Annex Journal Collection (LSA4) (TA.J86.H437)') + end + end + + test 'lookup returns empty list if no availability' do + VCR.use_cassette('alma sru no availability') do + needle = 'alma9935053423706761' + + result = AlmaSru.lookup(needle) + + assert_equal(result, []) + end + end + + test 'lookup returns empty list for non-existent records' do + output = StringIO.new + Rails.logger = Logger.new(output) + + VCR.use_cassette('alma sru nonexistent record') do + needle = 'alma9900000000006761' + + result = AlmaSru.lookup(needle) + + assert_equal(result, []) + + # This condition gets logged for local investigation due to control field mismatch + assert_match('Alma lookup failure: Control field mismatch', output.string) + end + end + + test 'lookup returns nil if env not set' do + needle = '990002935920106761' + ClimateControl.modify(MIT_ALMA_URL: nil) do + assert_equal([], AlmaSru.lookup(needle)) + end + end + + test 'lookup returns nil with non-complying ID' do + needle = 'foo' + + result = AlmaSru.lookup(needle) + + assert_equal([], result) + end + + test 'lookup survives failing to connect to Alma SRU' do + alma_client = AlmaConnectionError.new + output = StringIO.new + Rails.logger = Logger.new(output) + + needle = 'alma990014651640106761' + + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) + + assert_equal([], result) + + # This condition gets logged for local investigation + assert_match('Alma SRU connection error', output.string) + end + end + + test 'lookup survives Alma SRU errors' do + alma_client = AlmaErrorResponse.new + output = StringIO.new + Rails.logger = Logger.new(output) + + needle = 'alma990014651640106761' + + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) + + assert_equal(result, []) + + # This condition gets logged for local investigation + assert_match('Alma lookup failure: 500', output.string) + end + end + + test 'validate_alma_id succeeds with valid numeric input' do + needle = '990002935920106761' + assert_nothing_raised do + AlmaSru.validate_alma_id(needle) + end + end + + test 'validate_alma_id succeeds despite an "alma" prefix' do + needle = 'alma990002935920106761' + assert_nothing_raised do + AlmaSru.validate_alma_id(needle) + end + end + + test 'validate_alma_id raises InvalidAlmaId without required start sequence' do + assert_raises(AlmaSru::InvalidAlmaId) do + AlmaSru.validate_alma_id('0002935920106761') + end + + assert_raises(AlmaSru::InvalidAlmaId) do + AlmaSru.validate_alma_id('alma0002935920106761') + end + end + + test 'validate_alma_id raises InvalidAlmaId without required end sequence' do + assert_raises(AlmaSru::InvalidAlmaId) do + AlmaSru.validate_alma_id('99000293592010') + end + + assert_raises(AlmaSru::InvalidAlmaId) do + AlmaSru.validate_alma_id('alma99000293592010') + end + end + + test 'validate_alma_id raises InvalidAlmaId with wildly invalid input' do + assert_raises(AlmaSru::InvalidAlmaId) do + AlmaSru.validate_alma_id('foo') + end + end + + test 'fetch_controlfield isolates the controlfield with tag 001' do + needle = '990002935920106761' + + xml_content = File.read('test/fixtures/alma/sru_success.xml') + parsed = Nokogiri::XML(xml_content) + result = AlmaSru.fetch_controlfield(parsed) + + assert_equal(result, needle) + end + + test 'fetch_controlfield returns empty string if controlfield not found' do + xml_content = File.read('test/fixtures/alma/sru_nocontrol.xml') + parsed = Nokogiri::XML(xml_content) + result = AlmaSru.fetch_controlfield(parsed) + + assert_equal(result, '') + end + + test 'fetch_availabilities will list some libraries first' do + needle_first = 'Library Storage Annex' + needle_second = 'Barker Library' + + xml_content = File.read('test/fixtures/alma/sru_wrong_order.xml') + parsed = Nokogiri::XML(xml_content) + + raw_first = parsed.at_xpath("(//holding:datafield[@tag='AVA'])[1]/holding:subfield[@code='q']/text()", AlmaSru::NAMESPACE)&.text + raw_second = parsed.at_xpath("(//holding:datafield[@tag='AVA'])[2]/holding:subfield[@code='q']/text()", AlmaSru::NAMESPACE)&.text + + assert_equal(needle_first, raw_first) + assert_equal(needle_second, raw_second) + + result = AlmaSru.fetch_availabilities(parsed) + + assert_equal(needle_second, result[0]['q']) + assert_equal(needle_first, result[1]['q']) + end + + test 'format_availability returns availability in pattern of "E q c (d)"' do + ava_hash = { + 'c' => 'charlie', + 'd' => 'delta', + 'e' => 'echo', + 'q' => 'quebec' + } + + assert_equal('Echo at quebec charlie (delta)', AlmaSru.format_availability(ava_hash)) + end + + # Is this the behavior we want? + test 'format_availability does not error with missing fields' do + ava_hash = { + 'b' => 'beta' + } + + assert_equal(' at ()', AlmaSru.format_availability(ava_hash)) + end +end diff --git a/test/vcr_cassettes/alma_sru_multiple_records.yml b/test/vcr_cassettes/alma_sru_multiple_records.yml new file mode 100644 index 00000000..4ca48b6d --- /dev/null +++ b/test/vcr_cassettes/alma_sru_multiple_records.yml @@ -0,0 +1,407 @@ +--- +http_interactions: +- request: + method: get + uri: https://mit.alma.exlibrisgroup.com/view/sru/01MIT_INST?operation=searchRetrieve&query=alma.all_for_ui=990002935920106761&recordSchema=marcxml&version=1.2 + body: + encoding: ASCII-8BIT + string: '' + headers: + Connection: + - Keep-Alive + Host: + - mit.alma.exlibrisgroup.com + User-Agent: + - http.rb/5.3.1 + response: + status: + code: 200 + message: OK + headers: + X-Request-Id: + - lD4sRAGIpn + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID=9D331D7C05EEAF4BA7C5E8C9588C3009.app02.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; + Path=/; HttpOnly; SameSite=None; Secure + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1RuoKgw__XaR; Expires=Thu, + 05-Jun-2036 20:28:17 GMT; Path=/; Secure; SameSite=None + - urm_se=1780951097555; Path=/; SameSite=None; Secure + - urm_st=1780950497555; Path=/; SameSite=None; Secure + Access-Control-Allow-Methods: + - GET + Access-Control-Allow-Headers: + - "*" + Access-Control-Allow-Origin: + - "*" + Content-Security-Policy: + - 'object-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn www.google-analytics.com + stats.g.doubleclick.net s3.amazonaws.com www.youtube.com youtube.com *.contentdm.oclc.org + iiif.nlm.nih.gov ;worker-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn + www.google-analytics.com stats.g.doubleclick.net s3.amazonaws.com www.youtube.com + youtube.com artic.contentdm.oclc.org ;upgrade-insecure-requests; report-uri + /infra/CSPReportEndpoint.jsp; report-to csp-report-endpoint; ' + Report-To: + - '{"max_age":10886400,"endpoints":[{"url":"https://na06.alma.exlibrisgroup.com/infra/CSPReportEndpoint.jsp"}],"group":"csp-report-endpoint"}' + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Content-Type: + - text/xml;charset=UTF-8 + Transfer-Encoding: + - chunked + Date: + - Mon, 08 Jun 2026 20:28:17 GMT + Keep-Alive: + - timeout=20 + Connection: + - keep-alive + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: UTF-8 + string: |- + + 1.2 + 1 + + + marcxml + xml + + + 03917cas 2200889 a 4500 + 990002935920106761 + 20241127113448.0 + 741224c19599999nyuqr1p 0 a0eng^^ + + 61019573 //r892 + + + 0022-1481 + + + JHTRAO + + + 280380 + USPS + + + (MCM)000293592 + + + (MCM)000293592MIT01 + + + (OCoLC)01782922 + + + (OCoLC)01713702 + + + (OCoLC)09764105 + + + (VERA)3557 + + + ASME, United Engineering Center, 345 E. 47th St., New York, NY 10017 + + + DLC + DLC + NSD + DLC + NSD + SER + OCL + AIP + OCL + NSD + OCL + NST + DLC + MYG + + + lc + nsdp + + + MYGG + + + TA1 + .J64 + + + J. heat transfer + + + Journal of heat transfer + + + Journal of heat transfer. + + + New York, N.Y. : + American Society of Mechanical Engineers, + c1959- + + + v. : + ill. ; + 29 cm. + + + Quarterly + + + text + txt + rdacontent + + + unmediated + n + rdamedia + + + volume + nc + rdacarrier + + + Vol. 81, no. 1 (Feb. 1959)- + + + Transactions of the ASME ; + ser. C + + + Title from cover. + + + Applied science & technology index + 0003-6986 + + + Engineering index monthly (1984) + 0742-1974 + + + Engineering index bioengineering abstracts + 0736-6213 + + + Engineering index energy abstracts + 0093-8408 + + + Energy information abstracts + 0147-6521 + + + Environment abstracts + 0093-3287 + + + FLUIDEX + 1978- + + + Abstract bulletin of the Institute of Paper Chemistry + 0020-3033 + + + Computer & control abstracts + 0036-8113 + 1968- + + + Electrical & electronics abstracts + 0036-8105 + 1968- + + + Physics abstracts. Science abstracts. Series A + 0036-8091 + 1968- + + + Chemical abstracts + 0009-2258 + + + SPIN + 1977- + + + Coal abstracts + 0309-4979 + + + Energy research abstracts + 0160-3604 + + + International aerospace abstracts + 0020-5842 + + + Nuclear science abstracts + 0029-5612 + + + Indexes to publications - American Society of Mechanical Engineers + 0569-8227 + + + Vols. for 1978- lack series numeration. + + + Beginning 2000, full-text articles also available via the World Wide Web by subscription, in HTML, PDF, and PostScript formats. Tables of contents for 1996-1999 also available in HTML. + + + Also included in: American Society of Mechanical Engineers. Transactions of the American Society of Mechanical Engineers, 1959-<1967> + + + Heat + Transmission + Periodicals. + + + Heat + Transmission. + fast + + + Periodicals. + lcgft + + + Periodicals. + fast + + + Transactions of the ASME + (OCoLC)19731523 + (DLC)sc 89034060 + + + American Society of Mechanical Engineers. + Transactions of the American Society of Mechanical Engineers + (DLC) 02002053 + (OCoLC)1480830 + + + Journal of heat transfer [fiche] + E36211 960018634 + + + American Society of Mechanical Engineers. + + + Transactions of the ASME (1959) ; + ser. C. + + + jle001205/1 + + + MARCIVEAUT + + + depgy-nooclc210609 + 210609 + depgy-nooclc + + + MARCIVEAUT221103 + + + MARCIVEAUT231208 + + + MARCIVEAUT241112 + + + Vol. 102, no. 4 (Nov. 1980) LIC + + + unpiggy-tang + + + LSA + JRNAL + No Call # + 8 + 57 + All other volumes - use buttons to right + Please check availability at top of page to verify the Annex owns the volume/year you want. + ISSUE + + + New York, N.Y. : American Society of Mechanical Engineers, + 1959 + nyu + + + TA1 + JHTRAO + 01782922 + 01713702 + 09764105 + 61019573 //r892 + 0022-1481 + + + 02 + MYG + + + 990002935920106761 + 22477267920006761 + 01MIT_INST + LSA + Journal Collection (LSA4) + TA.J86.H437 + available + 63 + 0 + LSA4 + Hfcl + 1 + Library Storage Annex + v.81 (1959)-v.131:no.1-6 (2009) + + + 990002935920106761 + 22477267070006761 + 01MIT_INST + ENG + Staff Retrieval - request required + FICHE No Call # + check_holdings + MFORM + 8 + 2 + Barker Library + v.92 (1970)-v.95 (1973),v.97 (1975)-v.119 (1997) + + + + 990002935920106761 + 1 + + + + true + 2026-06-08T16:28:17-0400 + + + recorded_at: Mon, 08 Jun 2026 20:28:17 GMT +recorded_with: VCR 6.4.0 diff --git a/test/vcr_cassettes/alma_sru_no_availability.yml b/test/vcr_cassettes/alma_sru_no_availability.yml new file mode 100644 index 00000000..dcca3488 --- /dev/null +++ b/test/vcr_cassettes/alma_sru_no_availability.yml @@ -0,0 +1,286 @@ +--- +http_interactions: +- request: + method: get + uri: https://mit.alma.exlibrisgroup.com/view/sru/01MIT_INST?operation=searchRetrieve&query=alma.all_for_ui=9935053423706761&recordSchema=marcxml&version=1.2 + body: + encoding: ASCII-8BIT + string: '' + headers: + Connection: + - Keep-Alive + Host: + - mit.alma.exlibrisgroup.com + User-Agent: + - http.rb/5.3.1 + response: + status: + code: 200 + message: OK + headers: + X-Request-Id: + - xwR864Y1xM + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID=C3DDF913D7252F92452F5696B50F815B.app04.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; + Path=/; HttpOnly; SameSite=None; Secure + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1tuoKgw__f1o; Expires=Thu, + 05-Jun-2036 20:28:17 GMT; Path=/; Secure; SameSite=None + - urm_se=1780951097055; Path=/; SameSite=None; Secure + - urm_st=1780950497055; Path=/; SameSite=None; Secure + Access-Control-Allow-Methods: + - GET + Access-Control-Allow-Headers: + - "*" + Access-Control-Allow-Origin: + - "*" + Content-Security-Policy: + - 'object-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn www.google-analytics.com + stats.g.doubleclick.net s3.amazonaws.com www.youtube.com youtube.com *.contentdm.oclc.org + iiif.nlm.nih.gov ;worker-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn + www.google-analytics.com stats.g.doubleclick.net s3.amazonaws.com www.youtube.com + youtube.com artic.contentdm.oclc.org ;upgrade-insecure-requests; report-uri + /infra/CSPReportEndpoint.jsp; report-to csp-report-endpoint; ' + Report-To: + - '{"max_age":10886400,"endpoints":[{"url":"https://na06.alma.exlibrisgroup.com/infra/CSPReportEndpoint.jsp"}],"group":"csp-report-endpoint"}' + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Content-Type: + - text/xml;charset=UTF-8 + Transfer-Encoding: + - chunked + Date: + - Mon, 08 Jun 2026 20:28:17 GMT + Keep-Alive: + - timeout=20 + Connection: + - keep-alive + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: UTF-8 + string: |- + + 1.2 + 1 + + + marcxml + xml + + + 03192 am a2200565 4500 + 9935053423706761 + 20170117 + m o d + cu ||||||m|||| + 171205e2017||||xx |||||o|||||||||0|fre|d + + 2-917902-69-8 + + + 10.4000/books.inha.12276 + doi + + + (CKB)4100000009914036 + + + (FrMaCLE)OB-inha-12276 + + + (oapen)https://directory.doabooks.org/handle/20.500.12854/45032 + + + (FrMaCLE)OB-inha-7185 + + + (PPN)24129181X + + + (oapen)doab45032 + + + (EXLCZ)994100000009914036 + + + FR-FrMaCLE + + + fre + + + Awad, Alaa + + + Dialogues artistiques avec les passés de l'Égypte : + Une perspective transnationale et transmédiale / + Mercedes Volait, Emmanuelle Perrin. + + + Paris : + Publications de l’Institut national d’histoire de l’art, + 2017. + + + 1 online resource (235 p.) + + + text + txt + rdacontent + + + computer + c + rdamedia + + + online resource + cr + rdacarrier + + + Suez, Abou Simbel, Le Caire, Alger, Casablanca, Istanbul... Pour la première fois, des historiens de l'architecture et des conservateurs d'archives nous permettent d'accéder à un patrimoine culturel européen exceptionnel et méconnu : les archives produites par les entreprises du bâtiment et des travaux publics actives au sud de la Méditerranée, entre 1860 et 1970. Ouvrages d'art en acier ou béton armé, cités pour ouvriers et cadres expatriés, bâtiments publics mais aussi mobilier, décors, ouvrages effectués par des artisans d'art... Toutes ces réalisations témoignent d'une époque d'intenses échanges humains, techniques, et artistiques entre l'Europe et l'arc sud-est de la Méditerranée. Photographies anciennes destinées à promouvoir le travail des entrepreneurs, photographies de chantier, dessins d'architectes, croquis et carnets documentant les innovations techniques, plaquettes publicitaires... le livre est illustré par plus de 200 dessins et photographies provenant directement des fonds d'archives des constructeurs. Cet ouvrage est le résultat du projet de coopération transnationale "ARCHING : ARchives d'INGénierie européenne" (2010-2012) conduit dans le cadre du programme Culture 2007-2013 de la Commission européenne, auquel ont participé cinq institutions : l'Ecomusée du Bois-du-Luc (Belgique), la Cité de l'architecture et du patrimoine (France), InVisu (CNRS-INHA) (France), le Dipartimento di Architettura disegno-storia-progetto de l'université de Florence (Italie), Archmuseum (Turquie). Suez, Abu Simbel, Cairo, Algiers, Casablanca, Istanbul... This work of pioneering research by architectural historians and archivists gives us access to an exceptional field of European cultural heritage: the records of buildings and public works contractors active on the southern shores of the Mediterranean between 1860 and 1970. It covers all the construction trades, from steel or reinforced concrete bridges and dams, housing for laborers and expats, and public buildings,… + + + OpenEdition Books License + https://www.openedition.org/12554 + + + French + + + Architecture + + + architecte + + + entreprises de la construction + + + architecture + + + construction companies + + + egyptomania + + + historicism + + + painting + + + theater + + + decorative arts + + + heritage + + + architecture + + + Bardaouil, Sam + + + Bishop, Elizabeth + + + Chauffour, Sébastien + + + el-Wakil, Leïla + + + Fathy, Hassan + + + Garnier, Bénédicte + + + Humbert, Jean-Marcel + + + Khachab, Walid El + + + Ormos, István + + + Radwan, Nadia + + + Volait, Mercedes + + + Volait, Mercedes + + + Perrin, Emmanuelle + + + Mercedes Volait + aut + + + Emmanuelle Perrin + aut + + + 2-918371-12-2 + + + 979-1-0973-1500-9 + + + DOAB Library. + + + BOOK + + + RTCTFebook + CollConsol2026 + local + + + 53650867050006761 + 61535080440006761 + Available + DOAB Directory of Open Access Books + DOAB Directory of Open Access Books + 01MIT_INST + 9935053423706761 + + + 53542422790006761 + 61535080440006761 + Available + DOAB Directory of Open Access Books + DOAB Directory of Open Access Books + 01MIT_INST + 9935053423706761 + + + + 9935053423706761 + 1 + + + + true + 2026-06-08T16:28:17-0400 + + + recorded_at: Mon, 08 Jun 2026 20:28:17 GMT +recorded_with: VCR 6.4.0 diff --git a/test/vcr_cassettes/alma_sru_nonexistent_record.yml b/test/vcr_cassettes/alma_sru_nonexistent_record.yml new file mode 100644 index 00000000..be122013 --- /dev/null +++ b/test/vcr_cassettes/alma_sru_nonexistent_record.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: get + uri: https://mit.alma.exlibrisgroup.com/view/sru/01MIT_INST?operation=searchRetrieve&query=alma.all_for_ui=9900000000006761&recordSchema=marcxml&version=1.2 + body: + encoding: ASCII-8BIT + string: '' + headers: + Connection: + - Keep-Alive + Host: + - mit.alma.exlibrisgroup.com + User-Agent: + - http.rb/5.3.1 + response: + status: + code: 200 + message: OK + headers: + X-Request-Id: + - vjyUp6BOWm + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID=FCED354C1662C2CD3FE99660AA1094A3.app03.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; + Path=/; HttpOnly; SameSite=None; Secure + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1SeoKgw__ob6; Expires=Fri, + 06-Jun-2036 00:16:10 GMT; Path=/; Secure; SameSite=None + - urm_se=1780964770126; Path=/; SameSite=None; Secure + - urm_st=1780964170126; Path=/; SameSite=None; Secure + Access-Control-Allow-Methods: + - GET + Access-Control-Allow-Headers: + - "*" + Access-Control-Allow-Origin: + - "*" + Content-Security-Policy: + - 'object-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn www.google-analytics.com + stats.g.doubleclick.net s3.amazonaws.com www.youtube.com youtube.com *.contentdm.oclc.org + iiif.nlm.nih.gov ;worker-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn + www.google-analytics.com stats.g.doubleclick.net s3.amazonaws.com www.youtube.com + youtube.com artic.contentdm.oclc.org ;upgrade-insecure-requests; report-uri + /infra/CSPReportEndpoint.jsp; report-to csp-report-endpoint; ' + Report-To: + - '{"max_age":10886400,"endpoints":[{"url":"https://na06.alma.exlibrisgroup.com/infra/CSPReportEndpoint.jsp"}],"group":"csp-report-endpoint"}' + X-Content-Type-Options: + - nosniff + Content-Type: + - text/xml;charset=UTF-8 + Content-Length: + - '420' + Date: + - Tue, 09 Jun 2026 00:16:10 GMT + Keep-Alive: + - timeout=20 + Connection: + - keep-alive + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: UTF-8 + string: |- + + 1.2 + 0 + + + true + 2026-06-08T20:16:10-0400 + + + recorded_at: Tue, 09 Jun 2026 00:16:10 GMT +recorded_with: VCR 6.4.0 diff --git a/test/vcr_cassettes/alma_sru_single_record.yml b/test/vcr_cassettes/alma_sru_single_record.yml new file mode 100644 index 00000000..c5e6ad55 --- /dev/null +++ b/test/vcr_cassettes/alma_sru_single_record.yml @@ -0,0 +1,296 @@ +--- +http_interactions: +- request: + method: get + uri: https://mit.alma.exlibrisgroup.com/view/sru/01MIT_INST?operation=searchRetrieve&query=alma.all_for_ui=990014651640106761&recordSchema=marcxml&version=1.2 + body: + encoding: ASCII-8BIT + string: '' + headers: + Connection: + - Keep-Alive + Host: + - mit.alma.exlibrisgroup.com + User-Agent: + - http.rb/5.3.1 + response: + status: + code: 200 + message: OK + headers: + X-Request-Id: + - MsRANxfCdH + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID=0D60BA63676F7AE167C9631D032C1E48.app03.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; + Path=/; HttpOnly; SameSite=None; Secure + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1SeoKgw__ob6; Expires=Thu, + 05-Jun-2036 20:28:16 GMT; Path=/; Secure; SameSite=None + - urm_se=1780951096156; Path=/; SameSite=None; Secure + - urm_st=1780950496156; Path=/; SameSite=None; Secure + Access-Control-Allow-Methods: + - GET + Access-Control-Allow-Headers: + - "*" + Access-Control-Allow-Origin: + - "*" + Content-Security-Policy: + - 'object-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn www.google-analytics.com + stats.g.doubleclick.net s3.amazonaws.com www.youtube.com youtube.com *.contentdm.oclc.org + iiif.nlm.nih.gov ;worker-src blob: ''self'' *.exlibrisgroup.com *.exlibrisgroup.com.cn + www.google-analytics.com stats.g.doubleclick.net s3.amazonaws.com www.youtube.com + youtube.com artic.contentdm.oclc.org ;upgrade-insecure-requests; report-uri + /infra/CSPReportEndpoint.jsp; report-to csp-report-endpoint; ' + Report-To: + - '{"max_age":10886400,"endpoints":[{"url":"https://na06.alma.exlibrisgroup.com/infra/CSPReportEndpoint.jsp"}],"group":"csp-report-endpoint"}' + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Content-Type: + - text/xml;charset=UTF-8 + Transfer-Encoding: + - chunked + Date: + - Mon, 08 Jun 2026 20:28:16 GMT + Keep-Alive: + - timeout=20 + Connection: + - keep-alive + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + body: + encoding: UTF-8 + string: |- + + 1.2 + 1 + + + marcxml + xml + + + 02275cam 22005654a 4500 + 990014651640106761 + 20241127214106.0 + 060915s2007 mauae b 000 0deng^^ + + 2006030939 + + + GBA724048 + bnb + + + 013704232 + Uk + + + 9780262532914 (pbk. : alk. paper) + + + 0262532913 (pbk. : alk. paper) + + + (MCM)001465164MIT01 + + + (OCoLC)71427217 + + + DLC + DLC + BAKER + UKM + BTCTA + C#P + YDXCP + MYG + OrLoB-B + + + MYGG + + + NA680.C25 2007 + + + 724/.6 + 22 + + + Cadwell, Mike, + 1952- + + + Strange details / + Michael Cadwell. + + + Cambridge, Mass. : + MIT Press, + c2007. + + + xxi, 183 p. : + ill., plans ; + 21 cm. + + + text + txt + rdacontent + + + unmediated + n + rdamedia + + + volume + nc + rdacarrier + + + Writing architecture + + + Includes bibliographical references. + + + Introduction : "making strange" -- + 1. + Swimming at the Querini Stampali Foundation -- + 2. + The Jacobs House, Burning Fields -- + 3. + Flooded at the Farnsworth House -- + 4. + The Yale Center for British Art, yellow light and blue shadow. + + + Architecture, Modern + 20th century + Case studies. + + + Building + Case studies. + + + Building. + fast + + + Architecture, Modern. + fast + + + Case studies. + lcgft + + + Case studies. + fast + + + 20th century + fast + + + 20th century. + fast + + + Table of contents only + http://www.loc.gov/catdir/toc/ecip071/2006030939.html + + + Writing architecture. + + + pj070719 + pj + 070719 + + + MARCIVEAUT + + + MARCIVEAUT221103 + + + MARCIVEAUT231208 + + + MARCIVEAUT241113 + + + IP + r + RTC + STACK + 0 + 39080032070994 + 01 + NA680.C25 2007 + + + http://catalog.hathitrust.org/api/volumes/oclc/71427217.html + Hathi Trust + HathiETAS + + + Cambridge, Mass. : MIT Press, + 2007 + mau + + + 724/.6 + NA680.C25 2007 + 71427217 + 2006030939 + 9780262532914 (pbk. : alk. paper) + 0262532913 (pbk. : alk. paper) + + + digitized + 20230810 + HathiTrust + 005575003 + ic + + + 02 + MYG + + + 990014651640106761 + 22500544240006761 + 01MIT_INST + RTC + Stacks + NA680.C25 2007 + available + 1 + 0 + STACK + 0 + 1 + Rotch Library + + + + 990014651640106761 + 1 + + + + true + 2026-06-08T16:28:16-0400 + + + recorded_at: Mon, 08 Jun 2026 20:28:16 GMT +recorded_with: VCR 6.4.0 From bd445a47fbac3550f7abbae320b0c019f2bcf87c Mon Sep 17 00:00:00 2001 From: Matthew Bernhardt Date: Tue, 9 Jun 2026 10:22:19 -0400 Subject: [PATCH 2/4] Ignores .qlty materials --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f490baa9..2b4e6d75 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ .env .env.development +# qlty +.qlty + ## Environment normalization: /.bundle /vendor/bundle From bd2ef3596ac0cd880406ed388af20d1bb0fb6471 Mon Sep 17 00:00:00 2001 From: Matthew Bernhardt Date: Tue, 9 Jun 2026 11:22:18 -0400 Subject: [PATCH 3/4] Respond to Copilot feedback - Add env vars to README - Refactor guard clause for env into separate method, expand to include EXL_INST_ID - Fix typo in method comment - Restore logger to original state using begin/ensure blocks for those tests - Adds tests for updated guard clause - Update test names to reflect empty list return, not nil return - More robust test for env vars includes a confirmation of initial success - Adds test helper for scrubbing cassettes of cookie values - Scrubs cassettes --- README.md | 2 + app/models/alma_sru.rb | 18 +++-- test/models/alma_sru_test.rb | 70 ++++++++++++++----- test/test_helper.rb | 10 +++ .../alma_sru_multiple_records.yml | 18 +++-- .../alma_sru_no_availability.yml | 18 +++-- .../alma_sru_nonexistent_record.yml | 18 +++-- test/vcr_cassettes/alma_sru_single_record.yml | 18 +++-- 8 files changed, 109 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index a518c5bd..9a9cd5d4 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ See `Optional Environment Variables` for more information. ### Required Environment Variables - `ALMA_OPENURL`: The base URL for Alma openurls found in CDI records. +- `EXL_INST_ID`: The Ex Libris Institution ID. Used for constructing URLs. +- `MIT_ALMA_URL`: The base URL for MIT Libraries' Alma instance (used to generate SRU API lookups). - `MIT_PRIMO_URL`: The base URL for MIT Libraries' Primo instance (used to generate record links). - `PRIMO_API_KEY`: The Primo Search API key. - `PRIMO_API_URL`: The Primo Search API base URL. diff --git a/app/models/alma_sru.rb b/app/models/alma_sru.rb index d0031464..52615818 100644 --- a/app/models/alma_sru.rb +++ b/app/models/alma_sru.rb @@ -24,7 +24,7 @@ class InvalidAlmaId < StandardError; end # # It accepts an "alma_client" argument for use when testing, but this is not used in normal operations. def self.lookup(raw_identifier, alma_client: nil) - return [] unless alma_base_url + return [] unless alma_sru_enabled? # Validate the raw identifier received. This will raise an InvalidAlmaId if validation fails. identifier = validate_alma_id(raw_identifier) @@ -73,7 +73,7 @@ def self.parse_response(raw_response, reference_identifier) # validate_alma_id ensures we are only submitting valid Alma IDs to the SRU endpoint. # - # It needs to do two thigns: + # It needs to do two things: # 1. Remove the "alma" prefix if one is present. Otherwise, no manipulation of the submitted value should occur. # 2. Enforce the formatting requirements for a valid alma identifier (start with "99", and end with "6761"). def self.validate_alma_id(raw) @@ -129,12 +129,22 @@ def self.alma_base_url ENV.fetch('MIT_ALMA_URL', nil) end + def self.alma_sru_enabled? + return false if alma_base_url.to_s.empty? || exl_inst_id.to_s.empty? + + true + end + def self.alma_sru_url(identifier) - # example identifier: 990000959610106761 - "#{alma_base_url}/view/sru/#{ENV.fetch('EXL_INST_ID')}?version=1.2&operation=searchRetrieve&recordSchema=marcxml" \ + # example identifier: 9935177389906761 + "#{alma_base_url}/view/sru/#{exl_inst_id}?version=1.2&operation=searchRetrieve&recordSchema=marcxml" \ "&query=alma.all_for_ui=#{identifier}" end + def self.exl_inst_id + ENV.fetch('EXL_INST_ID', nil) + end + def self.setup(url, alma_client) alma_client || HTTP.persistent(url) end diff --git a/test/models/alma_sru_test.rb b/test/models/alma_sru_test.rb index 1c912eea..3bdf54c3 100644 --- a/test/models/alma_sru_test.rb +++ b/test/models/alma_sru_test.rb @@ -67,28 +67,50 @@ class AlmaSruTest < ActiveSupport::TestCase test 'lookup returns empty list for non-existent records' do output = StringIO.new + original_logger = Rails.logger Rails.logger = Logger.new(output) - VCR.use_cassette('alma sru nonexistent record') do - needle = 'alma9900000000006761' + begin + VCR.use_cassette('alma sru nonexistent record') do + needle = 'alma9900000000006761' - result = AlmaSru.lookup(needle) + result = AlmaSru.lookup(needle) - assert_equal(result, []) + assert_equal(result, []) - # This condition gets logged for local investigation due to control field mismatch - assert_match('Alma lookup failure: Control field mismatch', output.string) + # This condition gets logged for local investigation due to control field mismatch + assert_match('Alma lookup failure: Control field mismatch', output.string) + end + ensure + Rails.logger = original_logger end end - test 'lookup returns nil if env not set' do - needle = '990002935920106761' + test 'lookup returns empty list if alma URL not set' do + needle = 'alma990014651640106761' + + VCR.use_cassette('alma sru single record') do + assert_equal(1, AlmaSru.lookup(needle).length) + end + ClimateControl.modify(MIT_ALMA_URL: nil) do assert_equal([], AlmaSru.lookup(needle)) end end - test 'lookup returns nil with non-complying ID' do + test 'lookup returns empty list if exl_inst_id not set' do + needle = 'alma990014651640106761' + + VCR.use_cassette('alma sru single record') do + assert_equal(1, AlmaSru.lookup(needle).length) + end + + ClimateControl.modify(EXL_INST_ID: nil) do + assert_equal([], AlmaSru.lookup(needle)) + end + end + + test 'lookup returns empty list with non-complying ID' do needle = 'foo' result = AlmaSru.lookup(needle) @@ -99,34 +121,44 @@ class AlmaSruTest < ActiveSupport::TestCase test 'lookup survives failing to connect to Alma SRU' do alma_client = AlmaConnectionError.new output = StringIO.new + original_logger = Rails.logger Rails.logger = Logger.new(output) needle = 'alma990014651640106761' - assert_nothing_raised do - result = AlmaSru.lookup(needle, alma_client: alma_client) + begin + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) - assert_equal([], result) + assert_equal([], result) - # This condition gets logged for local investigation - assert_match('Alma SRU connection error', output.string) + # This condition gets logged for local investigation + assert_match('Alma SRU connection error', output.string) + end + ensure + Rails.logger = original_logger end end test 'lookup survives Alma SRU errors' do alma_client = AlmaErrorResponse.new output = StringIO.new + original_logger = Rails.logger Rails.logger = Logger.new(output) needle = 'alma990014651640106761' - assert_nothing_raised do - result = AlmaSru.lookup(needle, alma_client: alma_client) + begin + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) - assert_equal(result, []) + assert_equal(result, []) - # This condition gets logged for local investigation - assert_match('Alma lookup failure: 500', output.string) + # This condition gets logged for local investigation + assert_match('Alma lookup failure: 500', output.string) + end + ensure + Rails.logger = original_logger end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 35559b22..5c6ba377 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -32,6 +32,16 @@ config.filter_sensitive_data('FAKE_THIRDIRON_ID') { ENV.fetch('THIRDIRON_ID').to_s } config.filter_sensitive_data('FAKE_THIRDIRON_KEY') { ENV.fetch('THIRDIRON_KEY').to_s } config.filter_sensitive_data('FAKE_OPENALEX_EMAIL') { ENV.fetch('OPENALEX_EMAIL').to_s } + # Filter cookie contents + config.before_record do |interaction| + cookies = interaction.response&.headers&.fetch('Set-Cookie', nil) + next unless cookies + + interaction.response.headers['Set-Cookie'] = cookies.map do |cookie| + name = cookie.split('=', 2).first + "#{name}=" + end + end end module ActiveSupport diff --git a/test/vcr_cassettes/alma_sru_multiple_records.yml b/test/vcr_cassettes/alma_sru_multiple_records.yml index 4ca48b6d..b24a6e8e 100644 --- a/test/vcr_cassettes/alma_sru_multiple_records.yml +++ b/test/vcr_cassettes/alma_sru_multiple_records.yml @@ -19,16 +19,14 @@ http_interactions: message: OK headers: X-Request-Id: - - lD4sRAGIpn + - cefisv63dh P3p: - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" Set-Cookie: - - JSESSIONID=9D331D7C05EEAF4BA7C5E8C9588C3009.app02.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; - Path=/; HttpOnly; SameSite=None; Secure - - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1RuoKgw__XaR; Expires=Thu, - 05-Jun-2036 20:28:17 GMT; Path=/; Secure; SameSite=None - - urm_se=1780951097555; Path=/; SameSite=None; Secure - - urm_st=1780950497555; Path=/; SameSite=None; Secure + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= Access-Control-Allow-Methods: - GET Access-Control-Allow-Headers: @@ -53,7 +51,7 @@ http_interactions: Transfer-Encoding: - chunked Date: - - Mon, 08 Jun 2026 20:28:17 GMT + - Tue, 09 Jun 2026 16:01:06 GMT Keep-Alive: - timeout=20 Connection: @@ -400,8 +398,8 @@ http_interactions: true - 2026-06-08T16:28:17-0400 + 2026-06-09T12:01:06-0400 - recorded_at: Mon, 08 Jun 2026 20:28:17 GMT + recorded_at: Tue, 09 Jun 2026 16:01:06 GMT recorded_with: VCR 6.4.0 diff --git a/test/vcr_cassettes/alma_sru_no_availability.yml b/test/vcr_cassettes/alma_sru_no_availability.yml index dcca3488..9f845558 100644 --- a/test/vcr_cassettes/alma_sru_no_availability.yml +++ b/test/vcr_cassettes/alma_sru_no_availability.yml @@ -19,16 +19,14 @@ http_interactions: message: OK headers: X-Request-Id: - - xwR864Y1xM + - QE84k9NmR2 P3p: - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" Set-Cookie: - - JSESSIONID=C3DDF913D7252F92452F5696B50F815B.app04.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; - Path=/; HttpOnly; SameSite=None; Secure - - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1tuoKgw__f1o; Expires=Thu, - 05-Jun-2036 20:28:17 GMT; Path=/; Secure; SameSite=None - - urm_se=1780951097055; Path=/; SameSite=None; Secure - - urm_st=1780950497055; Path=/; SameSite=None; Secure + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= Access-Control-Allow-Methods: - GET Access-Control-Allow-Headers: @@ -53,7 +51,7 @@ http_interactions: Transfer-Encoding: - chunked Date: - - Mon, 08 Jun 2026 20:28:17 GMT + - Tue, 09 Jun 2026 16:01:07 GMT Keep-Alive: - timeout=20 Connection: @@ -279,8 +277,8 @@ http_interactions: true - 2026-06-08T16:28:17-0400 + 2026-06-09T12:01:07-0400 - recorded_at: Mon, 08 Jun 2026 20:28:17 GMT + recorded_at: Tue, 09 Jun 2026 16:01:07 GMT recorded_with: VCR 6.4.0 diff --git a/test/vcr_cassettes/alma_sru_nonexistent_record.yml b/test/vcr_cassettes/alma_sru_nonexistent_record.yml index be122013..e68d330a 100644 --- a/test/vcr_cassettes/alma_sru_nonexistent_record.yml +++ b/test/vcr_cassettes/alma_sru_nonexistent_record.yml @@ -19,16 +19,14 @@ http_interactions: message: OK headers: X-Request-Id: - - vjyUp6BOWm + - QBJY0Rl9Hk P3p: - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" Set-Cookie: - - JSESSIONID=FCED354C1662C2CD3FE99660AA1094A3.app03.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; - Path=/; HttpOnly; SameSite=None; Secure - - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1SeoKgw__ob6; Expires=Fri, - 06-Jun-2036 00:16:10 GMT; Path=/; Secure; SameSite=None - - urm_se=1780964770126; Path=/; SameSite=None; Secure - - urm_st=1780964170126; Path=/; SameSite=None; Secure + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= Access-Control-Allow-Methods: - GET Access-Control-Allow-Headers: @@ -51,7 +49,7 @@ http_interactions: Content-Length: - '420' Date: - - Tue, 09 Jun 2026 00:16:10 GMT + - Tue, 09 Jun 2026 16:01:06 GMT Keep-Alive: - timeout=20 Connection: @@ -67,8 +65,8 @@ http_interactions: true - 2026-06-08T20:16:10-0400 + 2026-06-09T12:01:06-0400 - recorded_at: Tue, 09 Jun 2026 00:16:10 GMT + recorded_at: Tue, 09 Jun 2026 16:01:06 GMT recorded_with: VCR 6.4.0 diff --git a/test/vcr_cassettes/alma_sru_single_record.yml b/test/vcr_cassettes/alma_sru_single_record.yml index c5e6ad55..2a9f5991 100644 --- a/test/vcr_cassettes/alma_sru_single_record.yml +++ b/test/vcr_cassettes/alma_sru_single_record.yml @@ -19,16 +19,14 @@ http_interactions: message: OK headers: X-Request-Id: - - MsRANxfCdH + - soQ4UmfGM7 P3p: - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" Set-Cookie: - - JSESSIONID=0D60BA63676F7AE167C9631D032C1E48.app03.na06.prod.alma.dc01.hosted.exlibrisgroup.com:1801; - Path=/; HttpOnly; SameSite=None; Secure - - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_=v1SeoKgw__ob6; Expires=Thu, - 05-Jun-2036 20:28:16 GMT; Path=/; Secure; SameSite=None - - urm_se=1780951096156; Path=/; SameSite=None; Secure - - urm_st=1780950496156; Path=/; SameSite=None; Secure + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= Access-Control-Allow-Methods: - GET Access-Control-Allow-Headers: @@ -53,7 +51,7 @@ http_interactions: Transfer-Encoding: - chunked Date: - - Mon, 08 Jun 2026 20:28:16 GMT + - Tue, 09 Jun 2026 15:59:03 GMT Keep-Alive: - timeout=20 Connection: @@ -289,8 +287,8 @@ http_interactions: true - 2026-06-08T16:28:16-0400 + 2026-06-09T11:59:03-0400 - recorded_at: Mon, 08 Jun 2026 20:28:16 GMT + recorded_at: Tue, 09 Jun 2026 15:59:03 GMT recorded_with: VCR 6.4.0 From f341e6b4b510a2ebb6d03d215051d6efecfa35a1 Mon Sep 17 00:00:00 2001 From: Matthew Bernhardt Date: Thu, 11 Jun 2026 16:21:59 -0400 Subject: [PATCH 4/4] Responding to code review feedback AlmaSru changes: - Add a `.to_s` statement during validation to protect against nil inputs - Added nuance around availability fields, enforcing specific requirements and better fallback behavior. - Refactoring InvalidAlmaId check to use regex and present? - Adding a Sentry capture if env isn't set Testing changes: - Adding tests to confirm behavior around edge cases (nil or unexpected inputs) - Ensure consistent order of "expected, actual" in assertions - Removing tests for logging output --- app/models/alma_sru.rb | 28 ++++++-- test/models/alma_sru_test.rb | 124 +++++++++++++++++++++-------------- 2 files changed, 96 insertions(+), 56 deletions(-) diff --git a/app/models/alma_sru.rb b/app/models/alma_sru.rb index 52615818..40eadc59 100644 --- a/app/models/alma_sru.rb +++ b/app/models/alma_sru.rb @@ -77,14 +77,13 @@ def self.parse_response(raw_response, reference_identifier) # 1. Remove the "alma" prefix if one is present. Otherwise, no manipulation of the submitted value should occur. # 2. Enforce the formatting requirements for a valid alma identifier (start with "99", and end with "6761"). def self.validate_alma_id(raw) - parsed = if raw.start_with?('alma') + parsed = if raw.to_s.start_with?('alma') raw.delete_prefix('alma') else raw end - raise InvalidAlmaId unless parsed.to_s.start_with?('99') - raise InvalidAlmaId unless parsed.to_s.end_with?('6761') + raise InvalidAlmaId unless parsed.present? && parsed.match?(/\A99\d+6761\z/) parsed end @@ -115,14 +114,28 @@ def self.fetch_availabilities(parsed_xml) # fetch_controlfield receives a parsed XML document (Nokogiri::XML::Document) # and returns the controlfield with an 001 tag, if one exists. + # + # This allows us to confirm that we've received the expected record back from + # the API, and not either a blank response or some other unexpected document. def self.fetch_controlfield(parsed_xml) parsed_xml.xpath("//holding:controlfield[@tag='001']", NAMESPACE)&.text end # format_availability receives a hash representing a single availability - # statement, and formats it for human readability. + # statement, and formats it for human readability. Values for "e" and "q" are + # required, while "c" and "d" are optional. + # + # A Sentry exception is captured if those required parameters are missing. def self.format_availability(availability) - "#{availability['e']&.humanize} at #{availability['q']} #{availability['c']} (#{availability['d']})" + if availability['e'].blank? || availability['q'].blank? + Sentry.capture_message('Missing required availability data') + return '' + end + + phrase = "#{availability['e']&.humanize} at #{availability['q']} #{availability['c']}".squish + phrase += " (#{availability['d']})" if availability['d'].present? + + phrase end def self.alma_base_url @@ -130,7 +143,10 @@ def self.alma_base_url end def self.alma_sru_enabled? - return false if alma_base_url.to_s.empty? || exl_inst_id.to_s.empty? + if alma_base_url.to_s.empty? || exl_inst_id.to_s.empty? + Sentry.capture_message('Alma SRU not enabled') + return false + end true end diff --git a/test/models/alma_sru_test.rb b/test/models/alma_sru_test.rb index 3bdf54c3..b73fe209 100644 --- a/test/models/alma_sru_test.rb +++ b/test/models/alma_sru_test.rb @@ -34,13 +34,14 @@ def get(_url) end class AlmaSruTest < ActiveSupport::TestCase + # Lookup method test 'lookup returns text for successful lookup' do VCR.use_cassette('alma sru single record') do needle = 'alma990014651640106761' result = AlmaSru.lookup(needle) - assert_equal(result, ['Available at Rotch Library Stacks (NA680.C25 2007)']) + assert_equal(['Available at Rotch Library Stacks (NA680.C25 2007)'], result) end end @@ -50,8 +51,8 @@ class AlmaSruTest < ActiveSupport::TestCase result = AlmaSru.lookup(needle) - assert_equal(result[0], 'Check holdings at Barker Library Staff Retrieval - request required (FICHE No Call #)') - assert_equal(result[1], 'Available at Library Storage Annex Journal Collection (LSA4) (TA.J86.H437)') + assert_equal('Check holdings at Barker Library Staff Retrieval - request required (FICHE No Call #)', result[0]) + assert_equal('Available at Library Storage Annex Journal Collection (LSA4) (TA.J86.H437)', result[1]) end end @@ -61,28 +62,17 @@ class AlmaSruTest < ActiveSupport::TestCase result = AlmaSru.lookup(needle) - assert_equal(result, []) + assert_equal([], result) end end test 'lookup returns empty list for non-existent records' do - output = StringIO.new - original_logger = Rails.logger - Rails.logger = Logger.new(output) + VCR.use_cassette('alma sru nonexistent record') do + needle = 'alma9900000000006761' - begin - VCR.use_cassette('alma sru nonexistent record') do - needle = 'alma9900000000006761' - - result = AlmaSru.lookup(needle) - - assert_equal(result, []) + result = AlmaSru.lookup(needle) - # This condition gets logged for local investigation due to control field mismatch - assert_match('Alma lookup failure: Control field mismatch', output.string) - end - ensure - Rails.logger = original_logger + assert_equal([], result) end end @@ -118,50 +108,47 @@ class AlmaSruTest < ActiveSupport::TestCase assert_equal([], result) end + test 'lookup returns empty list with empty string' do + needle = '' + + result = AlmaSru.lookup(needle) + + assert_equal([], result) + end + + test 'lookup returns empty list with nil input' do + needle = nil + + result = AlmaSru.lookup(needle) + + assert_equal([], result) + end + test 'lookup survives failing to connect to Alma SRU' do alma_client = AlmaConnectionError.new - output = StringIO.new - original_logger = Rails.logger - Rails.logger = Logger.new(output) needle = 'alma990014651640106761' - begin - assert_nothing_raised do - result = AlmaSru.lookup(needle, alma_client: alma_client) - - assert_equal([], result) + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) - # This condition gets logged for local investigation - assert_match('Alma SRU connection error', output.string) - end - ensure - Rails.logger = original_logger + assert_equal([], result) end end test 'lookup survives Alma SRU errors' do alma_client = AlmaErrorResponse.new - output = StringIO.new - original_logger = Rails.logger - Rails.logger = Logger.new(output) needle = 'alma990014651640106761' - begin - assert_nothing_raised do - result = AlmaSru.lookup(needle, alma_client: alma_client) - - assert_equal(result, []) + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) - # This condition gets logged for local investigation - assert_match('Alma lookup failure: 500', output.string) - end - ensure - Rails.logger = original_logger + assert_equal([], result) end end + # validate_alma_id method test 'validate_alma_id succeeds with valid numeric input' do needle = '990002935920106761' assert_nothing_raised do @@ -176,6 +163,21 @@ class AlmaSruTest < ActiveSupport::TestCase end end + test 'validate_alma_id raises InvalidAlmaId with non-numeric id' do + needle = '99000293foo5920106761' + + assert_raises(AlmaSru::InvalidAlmaId) do + AlmaSru.validate_alma_id(needle) + end + end + + test 'validate_alma_id raises InvalidAlmaId with a nil input' do + needle = nil + assert_raises(AlmaSru::InvalidAlmaId) do + AlmaSru.validate_alma_id(needle) + end + end + test 'validate_alma_id raises InvalidAlmaId without required start sequence' do assert_raises(AlmaSru::InvalidAlmaId) do AlmaSru.validate_alma_id('0002935920106761') @@ -202,6 +204,7 @@ class AlmaSruTest < ActiveSupport::TestCase end end + # fetch_controlfield method test 'fetch_controlfield isolates the controlfield with tag 001' do needle = '990002935920106761' @@ -209,7 +212,7 @@ class AlmaSruTest < ActiveSupport::TestCase parsed = Nokogiri::XML(xml_content) result = AlmaSru.fetch_controlfield(parsed) - assert_equal(result, needle) + assert_equal(needle, result) end test 'fetch_controlfield returns empty string if controlfield not found' do @@ -217,9 +220,10 @@ class AlmaSruTest < ActiveSupport::TestCase parsed = Nokogiri::XML(xml_content) result = AlmaSru.fetch_controlfield(parsed) - assert_equal(result, '') + assert_equal('', result) end + # fetch_availabilities method test 'fetch_availabilities will list some libraries first' do needle_first = 'Library Storage Annex' needle_second = 'Barker Library' @@ -239,6 +243,7 @@ class AlmaSruTest < ActiveSupport::TestCase assert_equal(needle_first, result[1]['q']) end + # format_availability method test 'format_availability returns availability in pattern of "E q c (d)"' do ava_hash = { 'c' => 'charlie', @@ -250,12 +255,31 @@ class AlmaSruTest < ActiveSupport::TestCase assert_equal('Echo at quebec charlie (delta)', AlmaSru.format_availability(ava_hash)) end - # Is this the behavior we want? - test 'format_availability does not error with missing fields' do + test 'format_availability returns a minimum statement if only e and q are present' do + ava_hash = { + 'e' => 'echo', + 'q' => 'quebec' + } + + assert_equal('Echo at quebec', AlmaSru.format_availability(ava_hash)) + end + + test 'format_availability returns an empty string without both e and q present' do ava_hash = { 'b' => 'beta' } - assert_equal(' at ()', AlmaSru.format_availability(ava_hash)) + assert_equal('', AlmaSru.format_availability(ava_hash)) + + ava_hash = { + 'e' => 'echo' + } + + assert_equal('', AlmaSru.format_availability(ava_hash)) + ava_hash = { + 'q' => 'quebec' + } + + assert_equal('', AlmaSru.format_availability(ava_hash)) end end