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/.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 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 new file mode 100644 index 00000000..40eadc59 --- /dev/null +++ b/app/models/alma_sru.rb @@ -0,0 +1,167 @@ +# 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_sru_enabled? + + # 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 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) + parsed = if raw.to_s.start_with?('alma') + raw.delete_prefix('alma') + else + raw + end + + raise InvalidAlmaId unless parsed.present? && parsed.match?(/\A99\d+6761\z/) + + 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. + # + # 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. 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) + 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 + ENV.fetch('MIT_ALMA_URL', nil) + end + + def self.alma_sru_enabled? + 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 + + def self.alma_sru_url(identifier) + # 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 +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..b73fe209 --- /dev/null +++ b/test/models/alma_sru_test.rb @@ -0,0 +1,285 @@ +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 + # 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(['Available at Rotch Library Stacks (NA680.C25 2007)'], result) + 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('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 + + 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 + VCR.use_cassette('alma sru nonexistent record') do + needle = 'alma9900000000006761' + + result = AlmaSru.lookup(needle) + + assert_equal([], result) + end + end + + 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 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) + + 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 + + needle = 'alma990014651640106761' + + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) + + assert_equal([], result) + end + end + + test 'lookup survives Alma SRU errors' do + alma_client = AlmaErrorResponse.new + + needle = 'alma990014651640106761' + + assert_nothing_raised do + result = AlmaSru.lookup(needle, alma_client: alma_client) + + 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 + 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 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') + 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 + + # fetch_controlfield method + 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(needle, result) + 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 + + # fetch_availabilities method + 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 + + # format_availability method + 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 + + 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('', 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 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 new file mode 100644 index 00000000..b24a6e8e --- /dev/null +++ b/test/vcr_cassettes/alma_sru_multiple_records.yml @@ -0,0 +1,405 @@ +--- +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: + - cefisv63dh + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= + 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: + - Tue, 09 Jun 2026 16:01:06 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-09T12:01:06-0400 + + + 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 new file mode 100644 index 00000000..9f845558 --- /dev/null +++ b/test/vcr_cassettes/alma_sru_no_availability.yml @@ -0,0 +1,284 @@ +--- +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: + - QE84k9NmR2 + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= + 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: + - Tue, 09 Jun 2026 16:01:07 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-09T12:01:07-0400 + + + 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 new file mode 100644 index 00000000..e68d330a --- /dev/null +++ b/test/vcr_cassettes/alma_sru_nonexistent_record.yml @@ -0,0 +1,72 @@ +--- +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: + - QBJY0Rl9Hk + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= + 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 16:01:06 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-09T12:01:06-0400 + + + 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 new file mode 100644 index 00000000..2a9f5991 --- /dev/null +++ b/test/vcr_cassettes/alma_sru_single_record.yml @@ -0,0 +1,294 @@ +--- +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: + - soQ4UmfGM7 + P3p: + - CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT" + Set-Cookie: + - JSESSIONID= + - __Secure-UqZBpD3n3naPR20-9Fvn5i-TQ-tFoshbYtbA9YCEpg3UXgo_= + - urm_se= + - urm_st= + 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: + - Tue, 09 Jun 2026 15:59:03 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-09T11:59:03-0400 + + + recorded_at: Tue, 09 Jun 2026 15:59:03 GMT +recorded_with: VCR 6.4.0