require 'spec_helper' require 'webmock' module BerkeleyLibrary module TIND module API describe Search do let(:base_uri) { 'https://tind.example.org/' } let(:api_key) { 'lorem-ipsum-dolor-sit-amet' } let(:query_uri) { BerkeleyLibrary::Util::URIs.append(base_uri, '/api/v1/search') } let(:headers) { { 'Authorization' => "Token #{api_key}" } } before(:each) do allow(BerkeleyLibrary::TIND::Config).to receive(:api_key).and_return(api_key) allow(BerkeleyLibrary::TIND::Config).to receive(:base_uri).and_return(base_uri) end describe 'single page' do let(:collection) { 'Bancroft Library' } let(:search) { Search.new(collection: collection) } before(:each) do body = File.read('spec/data/records-api-search.xml') stub_request(:get, query_uri) .with(headers: headers, query: { 'c' => collection, 'format' => 'xml' }) .to_return(status: 200, body: body) end describe :results do it 'returns the results' do results = search.results expect(results).to be_a(Array) expect(results.size).to eq(5) end end describe :each_result do it 'iterates over the results' do results = [] search.each_result { |r| results << r } expect(results.size).to eq(5) end it 'returns an enumerator' do results = [] enum = search.each_result expect(enum).to be_a(Enumerable) enum.each { |r| results << r } expect(results.size).to eq(5) end describe :freeze do describe 'with block' do it 'defaults to false' do search.each_result do |marc_record| expect(marc_record).not_to be_frozen end end it 'freezes results if true' do search.each_result(freeze: true) do |marc_record| expect(marc_record).to be_frozen end end end describe 'as enumerator' do it 'defaults to false' do enum = search.each_result enum.each do |marc_record| expect(marc_record).not_to be_frozen end end it 'freezes results if true' do enum = search.each_result(freeze: true) enum.each do |marc_record| expect(marc_record).to be_frozen end end end end end end describe 'multiple pages' do let(:collection) { 'Bancroft Library' } let(:search) { Search.new(collection: collection) } let(:search_id) { 'DnF1ZXJ5VGhlbkZldG' } let(:expected_count) { 554 } before(:each) do result_xml_pages = (1..7).map { |page| File.read("spec/data/records-api-search-p#{page}.xml") } query = { 'c' => collection, 'format' => 'xml' } stub_request(:get, query_uri) .with(headers: headers, query: query) .to_return(status: 200, body: result_xml_pages[0]) stubs = result_xml_pages[1..].map { |b| { status: 200, body: b } } stub_request(:get, query_uri).with( headers: headers, query: query.merge({ 'search_id' => search_id }) ).to_return(stubs) end describe :results do it 'returns the results' do results = search.results expect(results).to be_a(Array) expect(results.size).to eq(expected_count) first = results.first expect(first).to be_a(::MARC::Record) expect(first['024']['a']).to eq('BANC PIC 1958.021 Vol. 2:001--fALB') last = results.last expect(last).to be_a(::MARC::Record) expect(last['024']['a']).to eq('BANC PIC 1958.021 Vol. 3:162--fALB') end end describe :each_result do it 'iterates over the results' do results = [] search.each_result { |r| results << r } expect(results.size).to eq(expected_count) end it 'returns an enumerator' do results = [] enum = search.each_result expect(enum).to be_a(Enumerable) enum.each { |r| results << r } expect(results.size).to eq(expected_count) end describe :freeze do describe 'with block' do it 'defaults to false' do search.each_result do |marc_record| expect(marc_record).not_to be_frozen end end it 'freezes results if true' do search.each_result(freeze: true) do |marc_record| expect(marc_record).to be_frozen end end end describe 'as enumerator' do it 'defaults to false' do enum = search.each_result enum.each do |marc_record| expect(marc_record).not_to be_frozen end end it 'freezes results if true' do enum = search.each_result(freeze: true) enum.each do |marc_record| expect(marc_record).to be_frozen end end end end end end describe 'error handling' do describe 'Nonexistent collection' do let(:collection) { 'Not a collection' } let(:search) { Search.new(collection: collection) } let(:body) { '{"success": false, "error": "need more than 0 values to unpack"}' } before(:each) do stub_request(:get, query_uri) .with(headers: headers, query: { 'c' => collection, 'format' => 'xml' }) .to_return(status: 500, body: body, headers: { 'Content-Type' => 'applicaton/json' }) end describe :results do it 'returns an empty array' do results = search.results expect(results).to be_a(Array) expect(results.size).to eq(0) end end describe :each_result do it 'yields nothing' do expect { |b| search.each_result(&b) }.not_to yield_control end end end describe 'Empty result set' do let(:collection) { 'Restricted2Bancroft' } let(:search) { Search.new(collection: collection) } let(:body) do <<~XML 0 DnF1ZXJ5VGhlbkZldGNoBQAAAAACe5PjFmN0YjJMb0tKUTQtV1VfVzI2Qm8yY1EAAAAAABFKdxZuTnljT0hTU1FrMi1QSkNwVUEtZHJRAAAAAAIsSOcWSC0wU1Y0N2pRWUdodVBEdmdZUjBGUQAAAAACBSBOFjN6NGdTWWgzVFNpMGdQaDJyeGxBUncAAAAABEoXGhZ3SkFfY25BaFIzYVJMOGlDWnhxbHJn XML end before(:each) do stub_request(:get, query_uri) .with(headers: headers, query: { 'c' => collection, 'format' => 'xml' }) .to_return(status: 200, body: body, headers: { 'Content-Type' => 'applicaton/xml' }) end describe :results do it 'returns an empty array' do results = search.results expect(results).to be_a(Array) expect(results.size).to eq(0) end end describe :each_result do it 'yields nothing' do expect { |b| search.each_result(&b) }.not_to yield_control end end end describe 'Authn/Authz' do let(:collection) { 'Bancroft Library' } let(:search) { Search.new(collection: collection) } let(:params) { { 'c' => collection, 'format' => 'xml' } } describe 'insufficient privileges' do # NOTE: TIND seems to return 'guest' regardless of username in the insufficient-privileges case let(:body) { '{"error": "User guest is not authorized to perform runapi with parameters operation=read,endpoint=search"}' } before(:each) do @query_uri = BerkeleyLibrary::Util::URIs.append(base_uri, '/api/v1/search') stub_request(:get, query_uri) .with(headers: headers, query: params) .to_return(status: 403, body: body) end it 'raises an APIException' do expected_uri = BerkeleyLibrary::Util::URIs.append(query_uri, "?#{URI.encode_www_form(params)}") expect { search.results }.to raise_error do |e| expect(e).to be_a(APIException) expect(e.message).to include('403 Forbidden') expect(e.message).to include(expected_uri.to_s) expect(e.status_code).to eq(403) expect(e.status_message).to eq('403 Forbidden') end end end describe 'API key not set' do it 'raises an APIException without actually performing the search' do allow(BerkeleyLibrary::TIND::Config).to receive(:api_key).and_return(nil) expected_uri = BerkeleyLibrary::Util::URIs.append(query_uri, "?#{URI.encode_www_form(params)}") expect { search.results }.to raise_error do |e| expect(e).to be_a(APIException) expect(e.message).to include(expected_uri.to_s) expect(e.status_code).to eq(401) expect(e.status_message).to eq('401 Unauthorized') end end end describe 'invalid API key' do let(:body) { '{"error": "The entered api key is invalid"}' } before(:each) do @query_uri = BerkeleyLibrary::Util::URIs.append(base_uri, '/api/v1/search') stub_request(:get, query_uri) .with(headers: headers, query: params) .to_return(status: 401, body: body) end it 'raises an APIException' do expected_uri = BerkeleyLibrary::Util::URIs.append(query_uri, "?#{URI.encode_www_form(params)}") expect { search.results }.to raise_error do |e| expect(e).to be_a(APIException) expect(e.message).to include('401 Unauthorized') expect(e.message).to include(expected_uri.to_s) expect(e.status_code).to eq(401) expect(e.status_message).to eq('401 Unauthorized') end end end end describe '500 (some other error)' do let(:collection) { 'Bancroft Library' } let(:search) { Search.new(collection: collection) } let(:params) { { 'c' => collection, 'format' => 'xml' } } before(:each) do @query_uri = BerkeleyLibrary::Util::URIs.append(base_uri, '/api/v1/search') stub_request(:get, query_uri) .with(headers: headers, query: params) .to_return(status: 500, body: 'oops') end describe :results do it 'raises an APIException' do expected_uri = BerkeleyLibrary::Util::URIs.append(query_uri, "?#{URI.encode_www_form(params)}") expect { search.results }.to raise_error do |e| expect(e).to be_a(APIException) expect(e.message).to include('500 Internal Server Error') expect(e.message).to include(expected_uri.to_s) expect(e.status_code).to eq(500) expect(e.status_message).to eq('500 Internal Server Error') end end end describe :each_result do it 'yields nothing' do results = [] expect { search.each_result { |r| results << r } }.to raise_error do |e| expect(e).to be_a(APIException) expect(e.message).to include('500 Internal Server Error') expect(e.message).to include(query_uri.to_s) expect(e.status_code).to eq(500) expect(e.status_message).to eq('500 Internal Server Error') end expect(results).to be_empty end end end end it 'handles CJK' do collection = 'Houcun ju shi ji' search = Search.new(collection: collection) params = { 'c' => collection, 'format' => 'xml' } stub_request(:get, query_uri) .with(headers: headers, query: params) .to_return(status: 200, body: File.read('spec/data/records-api-search-cjk-p1.xml')) stub_request(:get, query_uri) .with(headers: headers, query: params.merge('search_id' => 'DnF1ZXJ5VGhlbkZldGNoBQAAAAAA')) .to_return(status: 200, body: File.read('spec/data/records-api-search-cjk-p2.xml')) results = search.results expect(results).to be_a(Array) expect(results.size).to eq(5) end end end end end