Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:

- name: Upload artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: artifacts
name: artifacts-${{ matrix.os }}-${{ matrix.ruby }}
path: artifacts/**
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 4.2.0 (2026-01-06)

- Rename RLF to SLF (NRLF -> SLFN, SRLF -> SLFS)
- Added bypass to skip SSL verification ONLY when recording new VCR cassettes

# 4.1.0 (2025-01-10)

- Fixed token refresh function and test
Expand Down
8 changes: 4 additions & 4 deletions lib/berkeley_library/location/location_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ def initialize(oclc_number, wc_symbols: [], wc_error: nil, ht_record_url: nil, h
@ht_error = ht_error
end

def nrlf?
@has_nrlf ||= wc_symbols.intersection(WorldCat::Symbols::NRLF).any?
def slfn?
@has_slfn ||= wc_symbols.intersection(WorldCat::Symbols::SLFN).any?
end

def srlf?
@has_srlf ||= wc_symbols.intersection(WorldCat::Symbols::SRLF).any?
def slfs?
@has_slfs ||= wc_symbols.intersection(WorldCat::Symbols::SLFS).any?
end

def uc_symbols
Expand Down
28 changes: 18 additions & 10 deletions lib/berkeley_library/location/world_cat/oclc_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,7 @@ def initialize
end

def fetch_token
url = oclc_token_url

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = url.scheme == 'https'

request = Net::HTTP::Post.new(url.request_uri)
request.basic_auth(Config.api_key, Config.api_secret)
request['Accept'] = 'application/json'
response = http.request(request)

response = http_request(oclc_token_url)
JSON.parse(response.body, symbolize_names: true)
end

Expand All @@ -42,6 +33,23 @@ def access_token

private

def http_request(url)
http = build_http(url)
request = Net::HTTP::Post.new(url.request_uri)
request.basic_auth(Config.api_key, Config.api_secret)
request['Accept'] = 'application/json'
http.request(request)
end

def build_http(url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = url.scheme == 'https'

# Skip SSL verification ONLY when recording new VCR cassettes
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if ENV['RE_RECORD_VCR'] == 'true'
http
end

def token_params
{
grant_type: 'client_credentials',
Expand Down
8 changes: 4 additions & 4 deletions lib/berkeley_library/location/world_cat/symbols.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ module BerkeleyLibrary
module Location
module WorldCat
module Symbols
NRLF = %w[ZAP ZAPSP].freeze
SRLF = %w[HH0 ZAS ZASSP].freeze
RLF = (NRLF + SRLF).freeze
SLFN = %w[ZAP ZAPSP].freeze
SLFS = %w[HH0 ZAS ZASSP].freeze
SLF = (SLFN + SLFS).freeze

UC = %w[CLU CRU CUI CUN CUS CUT CUV CUX CUY CUZ MERUC].freeze
ALL = (RLF + UC).freeze
ALL = (SLF + UC).freeze

class << self
include Symbols
Expand Down
38 changes: 19 additions & 19 deletions lib/berkeley_library/location/xlsx_writer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ class XLSXWriter
include Constants
include BerkeleyLibrary::Logging

COL_NRLF = 'NRLF'.freeze
COL_SRLF = 'SRLF'.freeze
COL_SLFN = 'SLFN'.freeze
COL_SLFS = 'SLFS'.freeze
COL_OTHER_UC = 'Other UC'.freeze
COL_WC_ERROR = 'WorldCat Error'.freeze

COL_HATHI_TRUST = 'Hathi Trust'.freeze
COL_HATHI_TRUST_ERROR = "#{COL_HATHI_TRUST} Error".freeze

V_NRLF = 'nrlf'.freeze
V_SRLF = 'srlf'.freeze
V_SLFN = 'slfn'.freeze
V_SLFS = 'slfs'.freeze

attr_reader :ss, :rlf, :uc, :hathi_trust
attr_reader :ss, :slf, :uc, :hathi_trust

def initialize(ss, rlf: true, uc: true, hathi_trust: true)
def initialize(ss, slf: true, uc: true, hathi_trust: true)
@ss = ss
@rlf = rlf
@slf = slf
@uc = uc
@hathi_trust = hathi_trust

Expand All @@ -32,7 +32,7 @@ def initialize(ss, rlf: true, uc: true, hathi_trust: true)
def <<(result)
r_indices = row_indices_for(result.oclc_number)
r_indices.each do |idx|
write_wc_cols(idx, result) if rlf || uc
write_wc_cols(idx, result) if slf || uc
write_ht_cols(idx, result) if hathi_trust
end
end
Expand All @@ -41,7 +41,7 @@ def <<(result)

def write_wc_cols(r_index, result)
write_wc_error(r_index, result)
write_rlf(r_index, result) if rlf
write_slf(r_index, result) if slf
write_uc(r_index, result) if uc
end

Expand All @@ -51,9 +51,9 @@ def write_ht_cols(r_index, result)
end

def ensure_columns!
if rlf
nrlf_col_index
srlf_col_index
if slf
slfn_col_index
slfs_col_index
end
uc_col_index if uc
ht_col_index if hathi_trust
Expand All @@ -66,9 +66,9 @@ def row_indices_for(oclc_number)
raise ArgumentError, "Unknown OCLC number: #{oclc_number}"
end

def write_rlf(r_index, result)
ss.set_value_at(r_index, nrlf_col_index, V_NRLF) if result.nrlf?
ss.set_value_at(r_index, srlf_col_index, V_SRLF) if result.srlf?
def write_slf(r_index, result)
ss.set_value_at(r_index, slfn_col_index, V_SLFN) if result.slfn?
ss.set_value_at(r_index, slfs_col_index, V_SLFS) if result.slfs?
end

def write_uc(r_index, result)
Expand Down Expand Up @@ -99,12 +99,12 @@ def oclc_col_index
@oclc_col_index ||= ss.find_column_index_by_header!(OCLC_COL_HEADER)
end

def nrlf_col_index
@nrlf_col_index ||= ss.ensure_column!(COL_NRLF)
def slfn_col_index
@slfn_col_index ||= ss.ensure_column!(COL_SLFN)
end

def srlf_col_index
@srlf_col_index ||= ss.ensure_column!(COL_SRLF)
def slfs_col_index
@slfs_col_index ||= ss.ensure_column!(COL_SLFS)
end

def uc_col_index
Expand Down
25 changes: 15 additions & 10 deletions spec/berkeley_library/location/world_cat/libraries_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ module Location
module WorldCat
describe LibrariesRequest do
let(:oclc_number) { '85833285' }
# let(:wc_base_url) { 'https://www.example.test/webservices/' }
let(:wc_base_url) { 'https://americas.discovery.api.oclc.org/worldcat/search/v2/' }
let(:wc_api_key) { '2lo55pdh7moyfodeo4gwgms0on65x31ghv0g6yg87ffwaljsdw' }
let(:wc_api_secret) { 'totallyfakesecret' }

# before do
# # Config.base_uri = wc_base_url
# # Config.api_key = wc_api_key
# # Config.api_secret = wc_api_secret
# end
before do
fake_auth = instance_double(
BerkeleyLibrary::Location::WorldCat::OCLCAuth,
access_token: 'fake-access-token'
)

allow(BerkeleyLibrary::Location::WorldCat::OCLCAuth)
.to receive(:instance)
.and_return(fake_auth)
end

after do
Config.send(:reset!)
BerkeleyLibrary::Location::WorldCat::OCLCAuth
.instance_variable_set(:@singleton__instance__, nil)
end

describe :new do
Expand Down Expand Up @@ -65,7 +70,7 @@ module WorldCat
end

it 'rejects an array containing nonexistent symbols' do
bad_symbols = [Symbols::NRLF, ['not a WorldCat institution symbol'], Symbols::SRLF].flatten
bad_symbols = [Symbols::SLFN, ['not a WorldCat institution symbol'], Symbols::SLFS].flatten
expect { LibrariesRequest.new(oclc_number, symbols: bad_symbols) }.to raise_error(ArgumentError)
end
end
Expand All @@ -92,7 +97,7 @@ module WorldCat

it 'returns a specified subset of holdings' do
holdings_expected = %w[ZAP]
symbols = Symbols::RLF
symbols = Symbols::SLF
req = LibrariesRequest.new(oclc_number, symbols:)

VCR.use_cassette('libraries_request/execute_holdings_2') do
Expand All @@ -116,7 +121,7 @@ module WorldCat

it 'returns an empty list when no holdings are found' do
oclc_number = '10045193'
symbols = Symbols::RLF
symbols = Symbols::SLF
req = LibrariesRequest.new(oclc_number, symbols:)

VCR.use_cassette('libraries_request/execute_holdings_4') do
Expand Down
82 changes: 68 additions & 14 deletions spec/berkeley_library/location/world_cat/oclc_auth_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
require 'spec_helper'
require 'time'
require 'active_support/testing/time_helpers'

module BerkeleyLibrary
module Location
module WorldCat
describe OCLCAuth do
include ActiveSupport::Testing::TimeHelpers

it 'fetches a token' do
VCR.use_cassette('oclc_auth/fetch_token') do
token = OCLCAuth.instance.token
Expand All @@ -14,25 +17,24 @@ module WorldCat
end

it 'refreshes an expired token' do
VCR.use_cassette('oclc_auth/refresh_token') do
# First get a token....
token = OCLCAuth.instance.token
freeze_time do
auth = OCLCAuth.instance

# Need to set the token expiration to a time in the past
token[:expires_at] = (Time.now - 60).to_s
token[:access_token] = 'expired_token'
# Simulate an expired token
expired_token = { access_token: 'expired_token', expires_at: (Time.current - 60).to_s }
auth.token = expired_token

# Now we need to set the token instance to the token with the updated expiration
OCLCAuth.instance.token = token
# Stub fetch_token to return a fresh token
new_token = { access_token: 'new_token', expires_at: (Time.current + 3600).to_s }
allow(auth).to receive(:fetch_token).and_return(new_token)

# Trigger a refresh by calling access_token
OCLCAuth.instance.access_token
result = auth.access_token

# Now check that the token has been refreshed
token = OCLCAuth.instance.token

expect(token[:access_token]).not_to eq('expired_token')
expect(Time.parse(token[:expires_at])).to be >= Time.now
# Check that the token was refreshed
expect(result).to eq('new_token')
expect(auth.token[:access_token]).to eq('new_token')
expect(Time.parse(auth.token[:expires_at])).to be >= Time.current
end
end

Expand All @@ -44,6 +46,58 @@ module WorldCat
expect(oclc_auth.send(:token_expired?)).to be true
end
end

describe '#fetch_token SSL branch' do
it 'does not disable SSL verification when RE_RECORD_VCR is not true' do
# Clear RE_RECORD_VCR
allow(ENV).to receive(:[]).with('RE_RECORD_VCR').and_return(nil)

url = URI('https://example.test/token')
http = instance_double(Net::HTTP)
allow(Net::HTTP).to receive(:new).and_return(http)
allow(http).to receive(:use_ssl=)
allow(http).to receive(:request).and_return(double(body: '{"access_token":"abc"}'))

auth = OCLCAuth.instance
allow(auth).to receive(:oclc_token_url).and_return(url)
allow(Config).to receive(:api_key).and_return('key')
allow(Config).to receive(:api_secret).and_return('secret')

expect(http).not_to receive(:verify_mode=)
auth.send(:fetch_token)
end

it 'disables SSL verification when RE_RECORD_VCR is true' do
# Force the env to true
allow(ENV).to receive(:[]).with('RE_RECORD_VCR').and_return('true')

url = URI('https://example.test/token')
http = instance_double(Net::HTTP)
allow(Net::HTTP).to receive(:new).and_return(http)
allow(http).to receive(:use_ssl=)
allow(http).to receive(:request).and_return(double(body: '{"access_token":"abc"}'))

auth = OCLCAuth.instance
allow(auth).to receive(:oclc_token_url).and_return(url)
allow(Config).to receive(:api_key).and_return('key')
allow(Config).to receive(:api_secret).and_return('secret')

# Expect that verify_mode is set when ENV is 'true'
expect(http).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
auth.send(:fetch_token)
end
end

describe '#access_token' do
it 'returns existing token if not expired' do
auth = OCLCAuth.instance
future_token = { access_token: 'valid-token', expires_at: (Time.now + 3600).to_s }
auth.token = future_token

expect(auth.access_token).to eq('valid-token')
end
end

end
end
end
Expand Down
Loading