diff --git a/app/controllers/api/v2/searches_controller.rb b/app/controllers/api/v2/searches_controller.rb index 904df79343..b396d6529e 100644 --- a/app/controllers/api/v2/searches_controller.rb +++ b/app/controllers/api/v2/searches_controller.rb @@ -79,17 +79,18 @@ def affiliate_docs_search_class end def handle_query_routing - return unless search_params[:query].present? and query_routing_is_enabled? affiliate = @search_options.site routed_query = affiliate.routed_queries .joins(:routed_query_keywords) .where(routed_query_keywords:{keyword: search_params[:query]}) .first - respond_with({ redirect: routed_query[:url] }, { status: 200 }) unless routed_query.nil? - end - def query_routing_is_enabled? - search_params[:routed] == 'true' + return unless routed_query + + RoutedQueryImpressionLogger.log(affiliate, + @search_options.query, request) + + respond_with({ route_to: routed_query[:url] }, { status: 200 }) end def search_params diff --git a/app/views/sites/api_instructions/_show_web_md.html.haml b/app/views/sites/api_instructions/_show_web_md.html.haml index 2e97be352f..c1a602af97 100644 --- a/app/views/sites/api_instructions/_show_web_md.html.haml +++ b/app/views/sites/api_instructions/_show_web_md.html.haml @@ -1,4 +1,6 @@ :markdown + + This API exposes all relevant results “modules” in a single JSON call, including: @@ -369,6 +371,44 @@ + ## Routed Queries + + + If you have [routed queries](https://search.gov/manual/routed-queries.html) set in your Admin page, then any matching query terms will change the API response. + + For example, if you set queries for `example` to route to `https://search.gov` then the following API call: + + `#{api_scheme_and_host}/api/v2/search?affiliate=#{h(@site.name)}&access_key=#{h(@site.api_access_key)}&query=example` + + Will then return this response: + + `{"route_to":"https://search.gov"}` + + With this response you can redirect your users to the url. Here is an example with javascript: + + ```javascript + var request = new XMLHttpRequest(); + var apiUrl = '#{api_scheme_and_host}/api/v2/search?affiliate=#{h(@site.name)}&access_key=#{h(@site.api_access_key)}&query=example' + + request.open('GET', apiUrl, true); + + request.onload = function() { + if (this.status >= 200 && this.status < 400) { + // Success! + var data = JSON.parse( this.response ); + window.location.replace( data.route_to ); + } else { + // We reached our target server, but it returned an error + } + }; + + request.onerror = function() { + // There was a connection error of some sort + }; + + request.send(); + ``` + ## I14y API diff --git a/spec/controllers/api/v2/searches_controller_spec.rb b/spec/controllers/api/v2/searches_controller_spec.rb index 22cb616c1b..6d87f102b7 100644 --- a/spec/controllers/api/v2/searches_controller_spec.rb +++ b/spec/controllers/api/v2/searches_controller_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' describe Api::V2::SearchesController do - fixtures :affiliates, :document_collections - let(:affiliate) { mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en) } + let(:affiliate) { affiliates(:basic_affiliate) } let(:search_params) do - { affiliate: 'usagov', - access_key: 'usagov_key', + { affiliate: 'nps.gov', + access_key: 'basic_key', format: 'json', api_key: 'myawesomekey', query: 'api', @@ -63,7 +62,6 @@ let!(:search) { double(ApiAzureSearch, as_json: { foo: 'bar'}, modules: %w(AWEB)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en) expect(Affiliate).to receive(:find_by_name).and_return(affiliate) expect(ApiAzureSearch).to receive(:new).with(hash_including(query_params)).and_return(search) @@ -73,9 +71,7 @@ hash_including('query'), be_a_kind_of(ActionDispatch::Request)) - get :azure, - params: search_params. - merge(access_key: 'usagov_key') + get :azure, params: search_params end it { is_expected.to respond_with :success } @@ -85,22 +81,18 @@ end end - context 'when the search options are valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) - get :azure, - params: search_params.merge(query: 'foo bar', routed: 'true') + get :azure, params: search_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end @@ -120,7 +112,6 @@ let!(:search) { double(ApiAzureCompositeWebSearch, as_json: { foo: 'bar'}, modules: %w(AZCW)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en) expect(Affiliate).to receive(:find_by_name).and_return(affiliate) expect(ApiAzureCompositeWebSearch).to receive(:new). @@ -131,7 +122,7 @@ hash_including('query'), be_a_kind_of(ActionDispatch::Request)) - get :azure_web, params: search_params.merge(access_key: 'usagov_key') + get :azure_web, params: search_params end it { is_expected.to respond_with :success } @@ -141,21 +132,19 @@ end end - context 'when the search options are valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) + - get :azure_web, params: search_params.merge(query: 'foo bar', routed: 'true') + get :azure_web, params: search_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end @@ -175,7 +164,6 @@ let!(:search) { double(ApiAzureCompositeImageSearch, as_json: { foo: 'bar'}, modules: %w(AZCI)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en) expect(Affiliate).to receive(:find_by_name).and_return(affiliate) expect(ApiAzureCompositeImageSearch).to receive(:new). @@ -196,21 +184,18 @@ end end - context 'when the search options are valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) - get :azure_image, params: search_params.merge(query: 'foo bar', routed: 'true') + get :azure_image, params: search_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end @@ -232,7 +217,6 @@ let!(:search) { double(ApiBingSearch, as_json: { foo: 'bar'}, modules: %w(BWEB)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en) expect(Affiliate).to receive(:find_by_name).and_return(affiliate) expect(ApiBingSearch).to receive(:new). @@ -253,21 +237,18 @@ end end - context 'when the search options are valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) - get :bing, params: bing_params.merge(query: 'foo bar', routed: 'true') + get :bing, params: bing_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end @@ -293,7 +274,6 @@ let!(:search) { double(ApiGssSearch, as_json: { foo: 'bar'}, modules: %w(GWEB)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en) expect(Affiliate).to receive(:find_by_name).and_return(affiliate) expect(ApiGssSearch).to receive(:new).with(hash_including(:query => 'api')).and_return(search) @@ -313,21 +293,18 @@ end end - context 'when the search options are not valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) - get :gss, params: gss_params.merge(query: 'foo bar', routed: 'true') + get :gss, params: gss_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end @@ -337,7 +314,7 @@ before do get :i14y, params: { - affiliate: 'usagov', + affiliate: 'nps.gov', format: 'json', query: 'api' } @@ -371,7 +348,7 @@ it 'passes the correct options to its ApiI4ySearch object' do expect(assigns(:search_options).attributes).to include({ - access_key: 'usagov_key', + access_key: 'basic_key', affiliate: affiliate, enable_highlighting: true, file_type: 'pdf', @@ -388,21 +365,18 @@ end end - context 'when the search options are not valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) - get :i14y, params: search_params.merge(query: 'foo bar', routed: 'true') + get :i14y, params: search_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end @@ -412,7 +386,7 @@ before do get :video, params: { - affiliate: 'usagov', + affiliate: 'nps.gov', format: 'json', query: 'api' } @@ -430,7 +404,6 @@ let!(:search) { double(ApiVideoSearch, as_json: { foo: 'bar'}, modules: %w(VIDS)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en) expect(Affiliate).to receive(:find_by_name).and_return(affiliate) expect(ApiVideoSearch).to receive(:new).with(hash_including(query_params)).and_return(search) @@ -450,21 +423,18 @@ end end - context 'when the search options are not valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) - get :video, params: search_params.merge(query: 'foo bar', routed: 'true') + get :video, params: search_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end @@ -485,8 +455,8 @@ let!(:search) { double(ApiBingDocsSearch, as_json: { foo: 'bar'}, modules: %w(BWEB)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en, search_engine: 'BingV6') expect(Affiliate).to receive(:find_by_name).and_return(affiliate) + allow(affiliate).to receive(:search_engine).and_return("BingV6") expect(ApiBingDocsSearch).to receive(:new).with(hash_including(query_params)).and_return(search) expect(search).to receive(:run) @@ -510,8 +480,8 @@ let!(:document_collection) { double(DocumentCollection, too_deep_for_bing?: true) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en, search_engine: 'BingV6') expect(Affiliate).to receive(:find_by_name).and_return(affiliate) + allow(affiliate).to receive(:search_engine).and_return("BingV6") expect(DocumentCollection).to receive(:find).and_return(document_collection) @@ -536,8 +506,8 @@ let!(:search) { double(ApiGoogleDocsSearch, as_json: { foo: 'bar'}, modules: %w(GWEB)) } before do - affiliate = mock_model(Affiliate, api_access_key: 'usagov_key', locale: :en, search_engine: 'Google') expect(Affiliate).to receive(:find_by_name).and_return(affiliate) + allow(affiliate).to receive(:search_engine).and_return("Google") expect(ApiGoogleDocsSearch).to receive(:new).with(hash_including(query_params)).and_return(search) expect(search).to receive(:run) @@ -556,21 +526,18 @@ end end - context 'when the search options are valid and the routed flag is enabled' do - let(:affiliate) { affiliates(:usagov_affiliate) } - + context 'when a routed query term is matched' do before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! + expect(RoutedQueryImpressionLogger).to receive(:log). + with(affiliate, 'moar unclaimed money', an_instance_of(ActionController::TestRequest)) - get :docs, params: docs_params.merge(query: 'foo bar', routed: 'true') + get :docs, params: docs_params.merge(query: 'moar unclaimed money') end it { is_expected.to respond_with :success } it 'returns search JSON' do - expect(JSON.parse(response.body)['redirect']).to eq('http://www.gov.gov/foo.html') + expect(JSON.parse(response.body)['route_to']).to eq('https://www.usa.gov/unclaimed_money') end end end diff --git a/spec/controllers/searches_controller_spec.rb b/spec/controllers/searches_controller_spec.rb index c696392cba..e83f207bcc 100644 --- a/spec/controllers/searches_controller_spec.rb +++ b/spec/controllers/searches_controller_spec.rb @@ -1,9 +1,6 @@ require 'spec_helper' describe SearchesController do - fixtures :affiliates, :image_search_labels, :document_collections, :rss_feeds, :rss_feed_urls, - :navigations, :features, :news_items, :languages - let(:affiliate) { affiliates(:usagov_affiliate) } context 'when showing a new search' do @@ -103,22 +100,16 @@ context 'searching on a routed keyword' do let(:affiliate) { affiliates(:basic_affiliate) } context 'referrer does not match redirect url' do - before do - routed_query = affiliate.routed_queries.build(url: "http://www.gov.gov/foo.html", description: "testing") - routed_query.routed_query_keywords.build(keyword: 'foo bar') - routed_query.save! - end - it 'redirects to the proper url' do - get :index, params: { query: "foo bar", affiliate: affiliate.name } - expect(response).to redirect_to 'http://www.gov.gov/foo.html' + get :index, params: { query: "moar unclaimed money", affiliate: affiliate.name } + expect(response).to redirect_to 'https://www.usa.gov/unclaimed_money' end it 'logs the impression' do - expect(SearchImpression).to receive(:log) + expect(RoutedQueryImpressionLogger).to receive(:log) get :index, params: { - query: 'foo bar', + query: 'moar unclaimed money', affiliate: affiliate.name } end diff --git a/spec/models/routed_query_impression_logger_spec.rb b/spec/models/routed_query_impression_logger_spec.rb new file mode 100644 index 0000000000..86d8799491 --- /dev/null +++ b/spec/models/routed_query_impression_logger_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe RoutedQueryImpressionLogger do + let!(:mock_search) do + instance_double(RoutedQueryImpressionLogger::QueryRoutedSearch, + modules: ['QRTD'], + diagnostics: {}) + end + let(:affiliate) { affiliates(:basic_affiliate) } + let(:mock_request) do + double('request', + remote_ip: '1.2.3.4', + url: 'http://www.gov.gov/', + referer: 'http://www.gov.gov/ref', + user_agent: 'whatevs', + headers: {}) + end + + before do + allow(RoutedQueryImpressionLogger::QueryRoutedSearch).to receive(:new).with( + ['QRTD'], + {} + ).and_return(mock_search) + end + + describe '.log' do + it 'sets up the right params to log a search impression' do + allow(SearchImpression).to receive(:log) + + RoutedQueryImpressionLogger.log(affiliates(:basic_affiliate), + 'example of a routed query', + mock_request) + + expect(SearchImpression).to have_received(:log).with( + mock_search, + :web, + { affiliate: 'nps.gov', query: 'example of a routed query' }, + mock_request + ) + end + end +end diff --git a/spec/models/search_impression_spec.rb b/spec/models/search_impression_spec.rb index 6f699ced15..0e6b368ae8 100644 --- a/spec/models/search_impression_spec.rb +++ b/spec/models/search_impression_spec.rb @@ -1,30 +1,87 @@ +# frozen_string_literal: true + require 'spec_helper' -describe SearchImpression, ".log" do - context 'params contains key with period' do - let(:params) { { "foo" => "yep", "bar.blat" => "nope" } } - let(:search) { double(Search, modules: %w(BWEB), diagnostics: { AWEB: { snap: 'judgement' } }) } - let(:request) { double("request", remote_ip: '1.2.3.4', url: 'http://www.gov.gov/', referer: 'http://www.gov.gov/ref', user_agent: 'whatevs', headers: {}) } - - it 'omits that parameter' do - time = Time.now - allow(Time).to receive(:now).and_return time - expect(Rails.logger).to receive(:info).with("[Search Impression] {\"clientip\":\"1.2.3.4\",\"request\":\"http://www.gov.gov/\",\"referrer\":\"http://www.gov.gov/ref\",\"user_agent\":\"whatevs\",\"diagnostics\":[{\"snap\":\"judgement\",\"module\":\"AWEB\"}],\"time\":\"#{time.to_formatted_s(:db)}\",\"vertical\":\"web\",\"modules\":\"BWEB\",\"params\":{\"foo\":\"yep\"}}") - SearchImpression.log(search, "web", params, request) +describe SearchImpression do + describe '.log' do + let(:request) do + double('request', + remote_ip: '1.2.3.4', + url: 'http://www.gov.gov/', + referer: 'http://www.gov.gov/ref', + user_agent: 'whatevs', + headers: {}) end - end + let(:search) do + double(Search, + modules: ['BWEB'], + diagnostics: { AWEB: { snap: 'judgement' } }) + end + let(:params) { { 'foo' => 'yep' } } + let(:time) { Time.now } + + before do + allow(Time).to receive(:now).and_return(time) + allow(Rails.logger).to receive(:info) + + SearchImpression.log(search, 'web', params, request) + end + + context 'with regular params' do + it 'has the single expected log line' do + expect(Rails.logger).to have_received(:info).once + expect(Rails.logger).to have_received(:info).with( + '[Search Impression] {"clientip":"1.2.3.4",'\ + '"request":"http://www.gov.gov/",'\ + '"referrer":"http://www.gov.gov/ref",'\ + '"user_agent":"whatevs","diagnostics":'\ + '[{"snap":"judgement","module":"AWEB"}],'\ + "\"time\":\"#{time.to_formatted_s(:db)}\","\ + '"vertical":"web","modules":"BWEB",'\ + '"params":{"foo":"yep"}}' + ) + end + end + + context 'with routed query module and empty diagnostics' do + let(:search) { double(Search, modules: ['QRTD'], diagnostics: {}) } + + it 'has the expected log line parts' do + expect(Rails.logger).to have_received(:info).with( + include('"modules":"QRTD"', '"diagnostics":[]') + ) + end + end + + context 'params contains key with period' do + let(:params) { { 'foo' => 'yep', 'bar.blat' => 'nope' } } + + it 'omits that parameter' do + expect(Rails.logger).to have_received(:info).with( + include('"params":{"foo":"yep"}') + ) + end + end + + context 'headers contains X-Original-Request header' do + let(:request) do + double('request', + remote_ip: '1.2.3.4', + url: 'http://www.gov.gov/', + referer: 'http://www.gov.gov/ref', + user_agent: 'whatevs', + headers: { 'X-Original-Request' => 'http://test.gov' }) + end - context 'headers contains X-Original-Request header' do - let(:params) { { "foo" => "yep", "bar.blat" => "nope" } } - let(:search) { double(Search, modules: %w(BWEB), diagnostics: { AWEB: { snap: 'judgement' } }) } - let(:request) { double("request", remote_ip: '1.2.3.4', url: 'http://www.gov.gov/', referer: 'http://www.gov.gov/ref', user_agent: 'whatevs', headers: { 'X-Original-Request' => 'http://test.gov' }) } - - it 'should log a non-null value for original_request' do - time = Time.now - allow(Time).to receive(:now).and_return time - expect(Rails.logger).to receive(:info).with("[X-Original-Request] (\"http://test.gov\")") - expect(Rails.logger).to receive(:info).with("[Search Impression] {\"clientip\":\"1.2.3.4\",\"request\":\"http://test.gov\",\"referrer\":\"http://www.gov.gov/ref\",\"user_agent\":\"whatevs\",\"diagnostics\":[{\"snap\":\"judgement\",\"module\":\"AWEB\"}],\"time\":\"#{time.to_formatted_s(:db)}\",\"vertical\":\"web\",\"modules\":\"BWEB\",\"params\":{\"foo\":\"yep\"}}") - SearchImpression.log(search, "web", params, request) + it 'should log two lines, the original-request header and the search impression' do + expect(Rails.logger).to have_received(:info).twice + expect(Rails.logger).to have_received(:info).with( + '[X-Original-Request] ("http://test.gov")' + ) + expect(Rails.logger).to have_received(:info).with( + include('[Search Impression]', '"request":"http://test.gov"') + ) + end end end end