elastic-site-search 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +58 -0
  3. data/.gitignore +10 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +201 -0
  7. data/NOTICE.txt +3 -0
  8. data/README.md +418 -0
  9. data/Rakefile +1 -0
  10. data/elastic-site-search.gemspec +26 -0
  11. data/lib/data/ca-bundle.crt +3554 -0
  12. data/lib/elastic/site-search.rb +9 -0
  13. data/lib/elastic/site-search/client.rb +527 -0
  14. data/lib/elastic/site-search/configuration.rb +67 -0
  15. data/lib/elastic/site-search/exceptions.rb +11 -0
  16. data/lib/elastic/site-search/ext/backport-uri.rb +33 -0
  17. data/lib/elastic/site-search/request.rb +156 -0
  18. data/lib/elastic/site-search/result_set.rb +84 -0
  19. data/lib/elastic/site-search/sso.rb +22 -0
  20. data/lib/elastic/site-search/version.rb +5 -0
  21. data/logo-site-search.png +0 -0
  22. data/spec/client_spec.rb +728 -0
  23. data/spec/configuration_spec.rb +37 -0
  24. data/spec/fixtures/vcr/analytics_autoselects.yml +57 -0
  25. data/spec/fixtures/vcr/analytics_autoselects_with_document_type.yml +57 -0
  26. data/spec/fixtures/vcr/analytics_autoselects_with_document_type_and_time_range.yml +57 -0
  27. data/spec/fixtures/vcr/analytics_autoselects_with_time_range.yml +57 -0
  28. data/spec/fixtures/vcr/analytics_clicks.yml +57 -0
  29. data/spec/fixtures/vcr/analytics_clicks_with_document_type.yml +57 -0
  30. data/spec/fixtures/vcr/analytics_clicks_with_document_type_and_time_range.yml +57 -0
  31. data/spec/fixtures/vcr/analytics_clicks_with_time_range.yml +57 -0
  32. data/spec/fixtures/vcr/analytics_searches.yml +57 -0
  33. data/spec/fixtures/vcr/analytics_searches_with_document_type_and_time_range.yml +57 -0
  34. data/spec/fixtures/vcr/analytics_searches_with_time_range.yml +57 -0
  35. data/spec/fixtures/vcr/analytics_searchs_with_document_type.yml +57 -0
  36. data/spec/fixtures/vcr/analytics_top_no_result_queries.yml +57 -0
  37. data/spec/fixtures/vcr/analytics_top_no_result_queries_paginated.yml +57 -0
  38. data/spec/fixtures/vcr/analytics_top_queries.yml +57 -0
  39. data/spec/fixtures/vcr/analytics_top_queries_paginated.yml +57 -0
  40. data/spec/fixtures/vcr/analytics_top_queries_too_large.yml +53 -0
  41. data/spec/fixtures/vcr/async_create_or_update_document_failure.yml +48 -0
  42. data/spec/fixtures/vcr/async_create_or_update_document_success.yml +52 -0
  43. data/spec/fixtures/vcr/bulk_create_documents.yml +52 -0
  44. data/spec/fixtures/vcr/bulk_create_or_update_documents_failure.yml +47 -0
  45. data/spec/fixtures/vcr/bulk_create_or_update_documents_success.yml +52 -0
  46. data/spec/fixtures/vcr/bulk_create_or_update_documents_verbose_failure.yml +47 -0
  47. data/spec/fixtures/vcr/bulk_create_or_update_documents_verbose_success.yml +52 -0
  48. data/spec/fixtures/vcr/bulk_destroy_documents.yml +47 -0
  49. data/spec/fixtures/vcr/crawl_url.yml +47 -0
  50. data/spec/fixtures/vcr/create_document.yml +52 -0
  51. data/spec/fixtures/vcr/create_document_type.yml +47 -0
  52. data/spec/fixtures/vcr/create_domain.yml +47 -0
  53. data/spec/fixtures/vcr/create_engine.yml +47 -0
  54. data/spec/fixtures/vcr/create_or_update_document_create.yml +51 -0
  55. data/spec/fixtures/vcr/create_or_update_document_update.yml +51 -0
  56. data/spec/fixtures/vcr/create_user.yml +47 -0
  57. data/spec/fixtures/vcr/destroy_document.yml +43 -0
  58. data/spec/fixtures/vcr/destroy_document_type.yml +43 -0
  59. data/spec/fixtures/vcr/destroy_domain.yml +43 -0
  60. data/spec/fixtures/vcr/destroy_engine.yml +43 -0
  61. data/spec/fixtures/vcr/destroy_non_existent_document_type.yml +45 -0
  62. data/spec/fixtures/vcr/document_receipts_multiple.yml +48 -0
  63. data/spec/fixtures/vcr/document_receipts_multiple_complete.yml +48 -0
  64. data/spec/fixtures/vcr/document_type_search.yml +63 -0
  65. data/spec/fixtures/vcr/document_type_search_pagination.yml +47 -0
  66. data/spec/fixtures/vcr/document_type_suggest.yml +52 -0
  67. data/spec/fixtures/vcr/document_type_suggest_pagination.yml +47 -0
  68. data/spec/fixtures/vcr/engine_search.yml +67 -0
  69. data/spec/fixtures/vcr/engine_search_facets.yml +164 -0
  70. data/spec/fixtures/vcr/engine_search_pagination.yml +47 -0
  71. data/spec/fixtures/vcr/engine_suggest.yml +55 -0
  72. data/spec/fixtures/vcr/engine_suggest_pagination.yml +47 -0
  73. data/spec/fixtures/vcr/find_document.yml +50 -0
  74. data/spec/fixtures/vcr/find_document_type.yml +47 -0
  75. data/spec/fixtures/vcr/find_domain.yml +47 -0
  76. data/spec/fixtures/vcr/find_domain_failure.yml +45 -0
  77. data/spec/fixtures/vcr/find_engine.yml +47 -0
  78. data/spec/fixtures/vcr/list_document_type.yml +47 -0
  79. data/spec/fixtures/vcr/list_documents.yml +71 -0
  80. data/spec/fixtures/vcr/list_documents_with_pagination.yml +71 -0
  81. data/spec/fixtures/vcr/list_domains.yml +47 -0
  82. data/spec/fixtures/vcr/list_engines.yml +48 -0
  83. data/spec/fixtures/vcr/list_users.yml +47 -0
  84. data/spec/fixtures/vcr/list_users_with_pagination.yml +47 -0
  85. data/spec/fixtures/vcr/log_clickthrough_failure.yml +45 -0
  86. data/spec/fixtures/vcr/log_clickthrough_success.yml +47 -0
  87. data/spec/fixtures/vcr/recrawl_domain_failure.yml +46 -0
  88. data/spec/fixtures/vcr/recrawl_domain_success.yml +47 -0
  89. data/spec/fixtures/vcr/show_user.yml +47 -0
  90. data/spec/fixtures/vcr/update_document.yml +50 -0
  91. data/spec/fixtures/vcr/update_document_unknown_field_failure.yml +48 -0
  92. data/spec/fixtures/vcr/update_documents_failure_non_existent_document.yml +47 -0
  93. data/spec/fixtures/vcr/update_documents_success.yml +47 -0
  94. data/spec/fixtures/vcr/users_client_secret_incorrect.yml +44 -0
  95. data/spec/fixtures/vcr/users_no_api_key.yml +44 -0
  96. data/spec/fixtures/vcr/users_no_client_id_or_secret.yml +45 -0
  97. data/spec/platform_spec.rb +95 -0
  98. data/spec/spec_helper.rb +27 -0
  99. data/spec/ssl_spec.rb +34 -0
  100. data/spec/sso_spec.rb +24 -0
  101. metadata +279 -0
@@ -0,0 +1,67 @@
1
+ require 'uri'
2
+ require 'elastic/site-search/version'
3
+
4
+ module Elastic
5
+ module SiteSearch
6
+ module Configuration
7
+ DEFAULT_ENDPOINT = "https://api.swiftype.com/api/v1/"
8
+
9
+ VALID_OPTIONS_KEYS = [
10
+ :api_key,
11
+ :user_agent,
12
+ :platform_client_id,
13
+ :platform_client_secret,
14
+ :endpoint
15
+ ]
16
+
17
+ attr_accessor *VALID_OPTIONS_KEYS
18
+
19
+ def self.extended(base)
20
+ base.reset
21
+ end
22
+
23
+ # Reset configuration to default values.
24
+ def reset
25
+ self.api_key = nil
26
+ self.endpoint = DEFAULT_ENDPOINT
27
+ self.user_agent = nil
28
+ self.platform_client_id = nil
29
+ self.platform_client_secret = nil
30
+ self
31
+ end
32
+
33
+ # Yields the Elastic::SiteSearch::Configuration module which can be used to set configuration options.
34
+ #
35
+ # @return self
36
+ def configure
37
+ yield self
38
+ self
39
+ end
40
+
41
+ # Return a hash of the configured options.
42
+ def options
43
+ options = {}
44
+ VALID_OPTIONS_KEYS.each{|k| options[k] = send(k)}
45
+ options
46
+ end
47
+
48
+ # Set api_key and endpoint based on a URL with HTTP authentication.
49
+ def authenticated_url=(url)
50
+ uri = URI(url)
51
+ self.api_key = uri.user
52
+ uri.user = nil
53
+ uri.password = nil
54
+ self.endpoint = uri.to_s
55
+ end
56
+
57
+ # setter for endpoint that ensures it always ends in '/'
58
+ def endpoint=(endpoint)
59
+ if endpoint.end_with?('/')
60
+ @endpoint = endpoint
61
+ else
62
+ @endpoint = "#{endpoint}/"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ module Elastic
2
+ module SiteSearch
3
+ class ClientException < StandardError; end
4
+ class NonExistentRecord < ClientException; end
5
+ class RecordAlreadyExists < ClientException; end
6
+ class InvalidCredentials < ClientException; end
7
+ class BadRequest < ClientException; end
8
+ class Forbidden < ClientException; end
9
+ class UnexpectedHTTPException < ClientException; end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ # :stopdoc:
2
+
3
+ # Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x
4
+ #
5
+ # https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb
6
+ #
7
+ #
8
+
9
+ module URI
10
+ def self.encode_www_form(enum)
11
+ enum.map do |k,v|
12
+ if v.nil?
13
+ encode_www_form_component(k)
14
+ elsif v.respond_to?(:to_ary)
15
+ v.to_ary.map do |w|
16
+ str = encode_www_form_component(k)
17
+ unless w.nil?
18
+ str << '='
19
+ str << encode_www_form_component(w)
20
+ end
21
+ end.join('&')
22
+ else
23
+ str = encode_www_form_component(k)
24
+ str << '='
25
+ str << encode_www_form_component(v)
26
+ end
27
+ end.join('&')
28
+ end
29
+
30
+ def self.encode_www_form_component(str)
31
+ str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/) { |chr| TBLENCWWWCOMP_[chr] }
32
+ end
33
+ end
@@ -0,0 +1,156 @@
1
+ require 'net/https'
2
+ if RUBY_VERSION < '1.9'
3
+ require 'elastic/site-search/ext/backport-uri'
4
+ else
5
+ require 'uri'
6
+ end
7
+ require 'json'
8
+ require 'elastic/site-search/exceptions'
9
+ require 'openssl'
10
+
11
+ module Elastic
12
+ module SiteSearch
13
+ CLIENT_NAME = 'elastic-site-search-ruby'
14
+ CLIENT_VERSION = Elastic::SiteSearch::VERSION
15
+
16
+ module Request
17
+ def get(path, params={})
18
+ request(:get, path, params)
19
+ end
20
+
21
+ def post(path, params={})
22
+ request(:post, path, params)
23
+ end
24
+
25
+ def put(path, params={})
26
+ request(:put, path, params)
27
+ end
28
+
29
+ def delete(path, params={})
30
+ request(:delete, path, params)
31
+ end
32
+
33
+ # Poll a block with backoff until a timeout is reached.
34
+ #
35
+ # @param [Hash] options optional arguments
36
+ # @option options [Numeric] :timeout (10) Number of seconds to wait before timing out
37
+ #
38
+ # @yieldreturn a truthy value to return from poll
39
+ # @yieldreturn [false] to continue polling.
40
+ #
41
+ # @return the truthy value returned from the block.
42
+ #
43
+ # @raise [Timeout::Error] when the timeout expires
44
+ def poll(options={})
45
+ timeout = options[:timeout] || 10
46
+ delay = 0.05
47
+ Timeout.timeout(timeout) do
48
+ while true
49
+ res = yield
50
+ return res if res
51
+ sleep delay *= 2
52
+ end
53
+ end
54
+ end
55
+
56
+ # Construct and send a request to the API.
57
+ #
58
+ # @raise [Timeout::Error] when the timeout expires
59
+ def request(method, path, params={})
60
+ Timeout.timeout(overall_timeout) do
61
+ uri = URI.parse("#{Elastic::SiteSearch.endpoint}#{path}")
62
+
63
+ request = build_request(method, uri, params)
64
+
65
+ if proxy
66
+ proxy_parts = URI.parse(proxy)
67
+ http = Net::HTTP.new(uri.host, uri.port, proxy_parts.host, proxy_parts.port, proxy_parts.user, proxy_parts.password)
68
+ else
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ end
71
+
72
+ http.open_timeout = open_timeout
73
+ http.read_timeout = overall_timeout
74
+
75
+ if uri.scheme == 'https'
76
+ http.use_ssl = true
77
+ # st_ssl_verify_none provides a means to disable SSL verification for debugging purposes. An example
78
+ # is Charles, which uses a self-signed certificate in order to inspect https traffic. This will
79
+ # not be part of this client's public API, this is more of a development enablement option
80
+ http.verify_mode = ENV['st_ssl_verify_none'] == 'true' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
81
+ http.ca_file = File.join(File.dirname(__FILE__), '../..', 'data', 'ca-bundle.crt')
82
+ http.ssl_timeout = open_timeout
83
+ end
84
+
85
+ response = http.request(request)
86
+ handle_errors(response)
87
+ JSON.parse(response.body) if response.body && response.body.strip != ''
88
+ end
89
+ end
90
+
91
+ private
92
+ def handle_errors(response)
93
+ case response
94
+ when Net::HTTPSuccess
95
+ response
96
+ when Net::HTTPUnauthorized
97
+ raise Elastic::SiteSearch::InvalidCredentials, error_message_from_response(response)
98
+ when Net::HTTPNotFound
99
+ raise Elastic::SiteSearch::NonExistentRecord, error_message_from_response(response)
100
+ when Net::HTTPConflict
101
+ raise Elastic::SiteSearch::RecordAlreadyExists, error_message_from_response(response)
102
+ when Net::HTTPBadRequest
103
+ raise Elastic::SiteSearch::BadRequest, error_message_from_response(response)
104
+ when Net::HTTPForbidden
105
+ raise Elastic::SiteSearch::Forbidden, error_message_from_response(response)
106
+ else
107
+ raise Elastic::SiteSearch::UnexpectedHTTPException, "#{response.code} #{response.body}"
108
+ end
109
+ end
110
+
111
+ def error_message_from_response(response)
112
+ body = response.body
113
+ json = JSON.parse(body) if body && body.strip != ''
114
+ return json['error'] if json && json.key?('error')
115
+ body
116
+ rescue JSON::ParserError
117
+ body
118
+ end
119
+
120
+ def build_request(method, uri, params)
121
+ klass = case method
122
+ when :get
123
+ Net::HTTP::Get
124
+ when :post
125
+ Net::HTTP::Post
126
+ when :put
127
+ Net::HTTP::Put
128
+ when :delete
129
+ Net::HTTP::Delete
130
+ end
131
+
132
+ case method
133
+ when :get, :delete
134
+ uri.query = URI.encode_www_form(params) if params && !params.empty?
135
+ req = klass.new(uri.request_uri)
136
+ when :post, :put
137
+ req = klass.new(uri.request_uri)
138
+ req.body = JSON.generate(params) unless params.length == 0
139
+ end
140
+
141
+ req['User-Agent'] = Elastic::SiteSearch.user_agent if Elastic::SiteSearch.user_agent
142
+ req['Content-Type'] = 'application/json'
143
+ req['X-Swiftype-Client'] = CLIENT_NAME
144
+ req['X-Swiftype-Client-Version'] = CLIENT_VERSION
145
+
146
+ if platform_access_token
147
+ req['Authorization'] = "Bearer #{platform_access_token}"
148
+ elsif api_key
149
+ req.basic_auth api_key, ''
150
+ end
151
+
152
+ req
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,84 @@
1
+ module Elastic
2
+ module SiteSearch
3
+ # The Elastic::SiteSearch::ResultSet class represents a {search}[https://swiftype.com/documentation/site-search/searching]
4
+ # or {suggest result}[https://swiftype.com/documentation/site-search/autocomplete] returned by the Elastic::SiteSearch API.
5
+ class ResultSet
6
+ # @attribute errors [r]
7
+ # a hash of errors for the search (for example filtering on a missing attribute) keyed by DocumentType slug
8
+ attr_reader :errors
9
+
10
+ # @attribute records [r]
11
+ # a hash of results for the search keyed by DocumentType slug. Use `[]` to access results more easily.
12
+ attr_reader :records
13
+
14
+ # @attribute info [r]
15
+ # a hash of extra query info (for example, facets and number of results) keyed by DocumentType slug.
16
+ # Use the convenience methods of this class for easier access.
17
+ attr_reader :info
18
+
19
+ # Create a Elastic::SiteSearch::ResultSet from deserialized JSON.
20
+ def initialize(results)
21
+ @records = results['records']
22
+ @info = results['info']
23
+ @errors = results['errors']
24
+ end
25
+
26
+ # Get results for the provided DocumentType
27
+ #
28
+ # @param [String] document_type the DocumentType slug to get results for
29
+ def [](document_type)
30
+ records[document_type]
31
+ end
32
+
33
+ # Return a list of DocumentType slugs represented in the ResultSet.
34
+ def document_types
35
+ records.keys
36
+ end
37
+
38
+ # Return the search facets for the provided DocumentType. Will be
39
+ # empty unless a facets parameter was provided when calling the
40
+ # search API.
41
+ #
42
+ # @param [String] document_type the DocumentType slug to get facets for
43
+ def facets(document_type)
44
+ info[document_type]['facets']
45
+ end
46
+
47
+ # Return the page of results for this ResultSet
48
+ def current_page
49
+ info[info.keys.first]['current_page']
50
+ end
51
+
52
+ # Return the number of results per page.
53
+ def per_page
54
+ info[info.keys.first]['per_page']
55
+ end
56
+
57
+ # Return the number of pages. Since a search can cover multiple
58
+ # DocumentTypes with different numbers of results, the number of
59
+ # pages can vary between DocumentTypes. With no argument, it
60
+ # returns the maximum num_pages for all DocumentTypes in this
61
+ # ResultSet. With a DocumentType slug, it returns the number of
62
+ # pages for that DocumentType.
63
+ #
64
+ # @param [String] document_type the DocumentType slug to return the number of pages for
65
+ def num_pages(document_type=nil)
66
+ if document_type
67
+ info[document_type]['num_pages']
68
+ else
69
+ info.values.map { |v| v['num_pages'] }.max
70
+ end
71
+ end
72
+
73
+ # Return the total number of results for the query
74
+ def total_result_count(document_type)
75
+ info[document_type]['total_result_count']
76
+ end
77
+
78
+ # Return the query used for this search
79
+ def query
80
+ info[info.keys.first]['query']
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,22 @@
1
+ require 'digest/sha1'
2
+
3
+ module Elastic
4
+ module SiteSearch
5
+ # Single sign-on for the Site Search Dashboard.
6
+ module SSO
7
+ BASE_URL = 'https://swiftype.com/sso'
8
+
9
+ # Generate a URL that a user can click on to be logged into the Site Search Dashboard.
10
+ # This requires the +platform_client_id+ and +platform_client_secret+ configuration options be set.
11
+ def self.url(user_id)
12
+ timestamp = Time.now.to_i
13
+
14
+ "#{BASE_URL}?user_id=#{user_id}&client_id=#{Elastic::SiteSearch.platform_client_id}&timestamp=#{timestamp}&token=#{token(user_id, timestamp)}"
15
+ end
16
+
17
+ def self.token(user_id, timestamp)
18
+ Digest::SHA1.hexdigest("#{user_id}:#{Elastic::SiteSearch.platform_client_secret}:#{timestamp}")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ module Elastic
2
+ module SiteSearch
3
+ VERSION = "2.0.0"
4
+ end
5
+ end
Binary file
@@ -0,0 +1,728 @@
1
+ require 'spec_helper'
2
+
3
+ describe Elastic::SiteSearch::Client do
4
+ let(:engine_slug) { 'site-search-api-example' }
5
+ let(:client) { Elastic::SiteSearch::Client.new }
6
+
7
+ before :each do
8
+ Elastic::SiteSearch.api_key = 'hello'
9
+ end
10
+
11
+ context 'Search' do
12
+ context '#search' do
13
+ it 'searches all DocumentTypes in the engine' do
14
+ VCR.use_cassette(:engine_search) do
15
+ results = client.search(engine_slug, 'cat')
16
+ expect(results.document_types.size).to eq(2)
17
+ expect(results['videos'].size).to eq(2)
18
+ expect(results['channels'].size).to eq(1)
19
+ end
20
+ end
21
+
22
+ it 'searches the engine with options' do
23
+ VCR.use_cassette(:engine_search_pagination) do
24
+ results = client.search(engine_slug, 'cat', {:page => 2})
25
+ expect(results.document_types.size).to eq(2)
26
+ expect(results['videos'].size).to eq(0)
27
+ expect(results['channels'].size).to eq(0)
28
+ end
29
+ end
30
+
31
+ it 'includes facets when requested' do
32
+ VCR.use_cassette(:engine_search_facets) do
33
+ results = client.search(engine_slug, nil, {:facets => {:videos => ['category_id']}})
34
+ expect(results.document_types.size).to eq(2)
35
+ expect(results.facets('channels')).to be_empty
36
+ expect(results.facets('videos')['category_id']).to eq({
37
+ "23" => 4,
38
+ "28" => 2,
39
+ "15" => 2,
40
+ "25" => 1,
41
+ "22" => 1,
42
+ "2" => 1,
43
+ "10" => 1
44
+ })
45
+ end
46
+ end
47
+ end
48
+
49
+ context '#search_document_type' do
50
+ let(:document_type_slug) { 'videos' }
51
+
52
+ it 'searches only the provided DocumentType' do
53
+ VCR.use_cassette(:document_type_search) do
54
+ results = client.search_document_type(engine_slug, document_type_slug, 'cat')
55
+ expect(results.document_types).to eq(['videos'])
56
+ expect(results['videos'].size).to eq(2)
57
+ end
58
+ end
59
+
60
+ it 'searches the DocumentType with options' do
61
+ VCR.use_cassette(:document_type_search_pagination) do
62
+ results = client.search_document_type(engine_slug, document_type_slug, 'cat', {:page => 2})
63
+ expect(results.document_types).to eq(['videos'])
64
+ expect(results[document_type_slug].size).to eq(0)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'Options' do
71
+ let(:options_client) { Elastic::SiteSearch::Client.new(options) }
72
+
73
+ context '#request' do
74
+ context 'open_timeout' do
75
+ let(:options) { { :open_timeout => 3 } }
76
+ it 'respects the Net::HTTP open_timeout option' do
77
+ expect(options_client.open_timeout).to eq(3)
78
+ end
79
+ end
80
+
81
+ context 'overall_timeout' do
82
+ context 'with timeout specified' do
83
+ let(:options) { { :overall_timeout => 0.001 } }
84
+ it 'respects the overall timeout option' do
85
+ expect(options_client.overall_timeout).to eq(0.001)
86
+ allow_any_instance_of(Net::HTTP).to receive(:request) { sleep 3 }
87
+ expect {
88
+ options_client.search(engine_slug, 'cat')
89
+ }.to raise_error(Timeout::Error)
90
+ end
91
+ end
92
+
93
+ context 'without timeout specified' do
94
+ let(:options) { Hash.new }
95
+ it 'omits the option' do
96
+ expect(options_client.overall_timeout).to eq(Elastic::SiteSearch::Client::DEFAULT_TIMEOUT.to_f)
97
+ expect(Timeout).to receive(:timeout).with(Elastic::SiteSearch::Client::DEFAULT_TIMEOUT.to_f).and_call_original
98
+ VCR.use_cassette(:engine_search) do
99
+ options_client.search(engine_slug, 'cat')
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ context 'with proxy specified' do
106
+ let(:options) { { :proxy => 'http://username:password@localhost:8888' } }
107
+
108
+ it 'will set proxy' do
109
+ expect(options_client.proxy).to eq('http://username:password@localhost:8888')
110
+ end
111
+
112
+ # There doesn't seem to be an elgant way to test that a request actually uses a proxy, so the best
113
+ # we can do here is ensure that the behavior for methods operates normally
114
+ it 'will execute methods with proxy' do
115
+ VCR.use_cassette(:engine_search) do
116
+ results = options_client.search(engine_slug, 'cat')
117
+ expect(results.document_types.size).to eq(2)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ context 'Suggest' do
125
+ context '#suggest' do
126
+ it 'does prefix searches for all DocumentTypes in the engine' do
127
+ VCR.use_cassette(:engine_suggest) do
128
+ results = client.suggest(engine_slug, 'goo')
129
+ expect(results.document_types.size).to eq(2)
130
+ expect(results['videos'].size).to eq(1)
131
+ expect(results['channels'].size).to eq(1)
132
+ end
133
+ end
134
+
135
+ it 'suggests for an engine with options' do
136
+ VCR.use_cassette(:engine_suggest_pagination) do
137
+ results = client.suggest(engine_slug, 'goo', {:page => 2})
138
+ expect(results.document_types.size).to eq(2)
139
+ expect(results['videos'].size).to eq(0)
140
+ expect(results['channels'].size).to eq(0)
141
+ end
142
+ end
143
+ end
144
+
145
+ context '#suggest_document_type' do
146
+ let(:document_type_slug) { 'videos' }
147
+
148
+ it 'does a prefix search on the provided DocumentType' do
149
+ VCR.use_cassette(:document_type_suggest) do
150
+ results = client.suggest_document_type(engine_slug, document_type_slug, 'goo')
151
+ expect(results.document_types).to eq(['videos'])
152
+ expect(results['videos'].size).to eq(1)
153
+ end
154
+ end
155
+
156
+ it 'suggests for a document types with options' do
157
+ VCR.use_cassette(:document_type_suggest_pagination) do
158
+ results = client.suggest_document_type(engine_slug, document_type_slug, 'goo', {:page => 2})
159
+ expect(results.document_types).to eq(['videos'])
160
+ expect(results[document_type_slug].size).to eq(0)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+
167
+ context 'Engine' do
168
+ it 'gets all engines' do
169
+ VCR.use_cassette(:list_engines) do
170
+ engines = client.engines
171
+ expect(engines.size).to eq(6)
172
+ end
173
+ end
174
+
175
+ it 'gets an engine' do
176
+ VCR.use_cassette(:find_engine) do
177
+ engine = client.engine(engine_slug)
178
+ expect(engine['slug']).to eq(engine_slug)
179
+ end
180
+ end
181
+
182
+ it 'creates engines' do
183
+ VCR.use_cassette(:create_engine) do
184
+ engine = client.create_engine('new engine from spec')
185
+ expect(engine['slug']).to eq('new-engine-from-spec')
186
+ end
187
+ end
188
+
189
+ it 'destroys engines' do
190
+ VCR.use_cassette(:destroy_engine) do
191
+ response = client.destroy_engine('new-engine-from-spec')
192
+ expect(response).to be_nil
193
+ end
194
+ end
195
+ end
196
+
197
+ context 'DocumentType' do
198
+ let(:document_type_slug) { 'videos' }
199
+
200
+ it 'gets all document types' do
201
+ VCR.use_cassette(:list_document_type) do
202
+ document_types = client.document_types(engine_slug)
203
+ expect(document_types.size).to eq(2)
204
+ expect(document_types.map { |dt| dt['name'] }.sort).to eq(['channels', 'videos'])
205
+ end
206
+ end
207
+
208
+ it 'gets a document type' do
209
+ VCR.use_cassette(:find_document_type) do
210
+ document_type = client.document_type(engine_slug, document_type_slug)
211
+ expect(document_type['slug']).to eq(document_type_slug)
212
+ end
213
+ end
214
+
215
+ it 'creates a document type' do
216
+ VCR.use_cassette(:create_document_type) do
217
+ name = document_type_slug
218
+ document_type = client.create_document_type(engine_slug, 'new_doc_type')
219
+ expect(document_type['name']).to eq('new_doc_type')
220
+ expect(document_type['slug']).to eq('new-doc-type')
221
+ end
222
+ end
223
+
224
+ it 'destroys document types' do
225
+ VCR.use_cassette(:destroy_document_type) do
226
+ response = client.destroy_document_type(engine_slug, 'new-doc-type')
227
+ expect(response).to be_nil
228
+ end
229
+ end
230
+
231
+ it 'raises an error if deleting a non-existent DocumentType' do
232
+ VCR.use_cassette(:destroy_non_existent_document_type) do
233
+ expect do
234
+ response = client.destroy_document_type(engine_slug, 'not_there')
235
+ end.to raise_error
236
+ end
237
+ end
238
+ end
239
+
240
+ context 'Document' do
241
+
242
+ def check_async_response_format(response, options = {})
243
+ expect(response.keys).to match_array(["document_receipts", "batch_link"])
244
+ expect(response["document_receipts"]).to be_a_kind_of(Array)
245
+ expect(response["document_receipts"].first.keys).to match_array(["id", "external_id", "link", "status", "errors"])
246
+ expect(response["document_receipts"].first["external_id"]).to eq(options[:external_id]) if options[:external_id]
247
+ expect(response["document_receipts"].first["status"]).to eq(options[:status]) if options[:status]
248
+ expect(response["document_receipts"].first["errors"]).to eq(options[:errors]) if options[:errors]
249
+ end
250
+
251
+ let(:document_type_slug) { 'videos' }
252
+ let(:document_id) { 'FtX8nswnUKU'}
253
+ let(:documents) do
254
+ [{'external_id'=>'INscMGmhmX4',
255
+ 'fields' => [{'name'=>'url', 'value'=>'http://www.youtube.com/watch?v=v1uyQZNg2vE', 'type'=>'enum'},
256
+ {'name'=>'thumbnail_url', 'value'=>'https://i.ytimg.com/vi/INscMGmhmX4/mqdefault.jpg', 'type'=>'enum'},
257
+ {'name'=>'channel_id', 'value'=>'UCTzVrd9ExsI3Zgnlh3_btLg', 'type'=>'enum'},
258
+ {'name'=>'title', 'value'=>'The Original Grumpy Cat', 'type'=>'string'},
259
+ {'name'=>'category_name', 'value'=>'Pets &amp; Animals', 'type'=>'string'}]},
260
+ {'external_id'=>'XfY9Dsg_DZk',
261
+ 'fields' => [{'name'=>'url', 'value'=>'http://www.youtube.com/watch?v=XfY9Dsg_DZk', 'type'=>'enum'},
262
+ {'name'=>'thumbnail_url', 'value'=>'https://i.ytimg.com/vi/XfY9Dsg_DZk/mqdefault.jpg', 'type'=>'enum'},
263
+ {'name'=>'channel_id', 'value'=>'UC5VA5j05FjETg-iLekcyiBw', 'type'=>'enum'},
264
+ {'name'=>'title', 'value'=>'Corgi talks to cat', 'type'=>'string'},
265
+ {'name'=>'category_name', 'value'=>'Pets &amp; Animals', 'type'=>'string'}]}]
266
+ end
267
+
268
+ it 'lists documents in a document type' do
269
+ VCR.use_cassette(:list_documents) do
270
+ documents = client.documents(engine_slug, document_type_slug)
271
+ expect(documents.size).to eq(2)
272
+ end
273
+ end
274
+
275
+ it 'lists documents with pagination' do
276
+ VCR.use_cassette(:list_documents_with_pagination) do
277
+ documents = client.documents(engine_slug, document_type_slug, 2, 2)
278
+ expect(documents.size).to eq(2)
279
+ end
280
+ end
281
+
282
+ it 'find a document' do
283
+ VCR.use_cassette(:find_document) do
284
+ document = client.document(engine_slug, document_type_slug, document_id)
285
+ expect(document['external_id']).to eq(document_id)
286
+ end
287
+ end
288
+
289
+ it 'creates a document' do
290
+ VCR.use_cassette(:create_document) do
291
+ document = client.create_document(engine_slug, document_type_slug, documents.first)
292
+ expect(document['external_id']).to eq('INscMGmhmX4')
293
+ end
294
+ end
295
+
296
+ it 'bulk create multiple documents' do
297
+ VCR.use_cassette(:bulk_create_documents) do
298
+ response = client.create_documents(engine_slug, document_type_slug, documents)
299
+ expect(response).to eq([true, true])
300
+ end
301
+ end
302
+
303
+ it 'destroys a document' do
304
+ VCR.use_cassette(:destroy_document) do
305
+ response = client.destroy_document(engine_slug, document_type_slug, 'INscMGmhmX4')
306
+ expect(response).to be_nil
307
+ end
308
+ end
309
+
310
+ it 'destroys multiple documents' do
311
+ VCR.use_cassette(:bulk_destroy_documents) do
312
+ response = client.destroy_documents(engine_slug, document_type_slug, ['INscMGmhmX4', 'XfY9Dsg_DZk'])
313
+ expect(response).to eq([true, true])
314
+ end
315
+ end
316
+
317
+ context '#create_or_update_document' do
318
+ it 'creates a document' do
319
+ VCR.use_cassette(:create_or_update_document_create) do
320
+ response = client.create_or_update_document(engine_slug, document_type_slug, {:external_id => 'foobar', :fields => [{:type => :string, :name => 'title', :value => 'new document'}]})
321
+ expect(response['external_id']).to eq('foobar')
322
+ expect(response['title']).to eq('new document')
323
+ end
324
+ end
325
+
326
+ it 'updates an existing document' do
327
+ VCR.use_cassette(:create_or_update_document_update) do
328
+ response = client.create_or_update_document(engine_slug, document_type_slug, {:external_id => document_id, :fields => [{:type => :string, :name => 'title', :value => 'new title'}]})
329
+ expect(response['external_id']).to eq(document_id)
330
+ expect(response['title']).to eq('new title')
331
+ end
332
+ end
333
+ end
334
+
335
+ context '#bulk_create_or_update_documents' do
336
+ it 'returns true for all documents successfully created or updated' do
337
+ VCR.use_cassette(:bulk_create_or_update_documents_success) do
338
+ response = client.create_or_update_documents(engine_slug, document_type_slug, documents)
339
+ expect(response).to eq([true, true])
340
+ end
341
+ end
342
+
343
+ it 'returns false if a document cannot be created or updated due to an error' do
344
+ documents = [{:external_id => 'failed_doc', :fields => [{:type => :string, :name => :title}]}] # missing value
345
+
346
+ VCR.use_cassette(:bulk_create_or_update_documents_failure) do
347
+ response = client.create_or_update_documents(engine_slug, document_type_slug, documents)
348
+ expect(response).to eq([false])
349
+ end
350
+ end
351
+ end
352
+
353
+ context '#async_create_or_update_documents' do
354
+ it 'returns true for all documents successfully created or updated' do
355
+ VCR.use_cassette(:async_create_or_update_document_success) do
356
+ response = client.async_create_or_update_documents(engine_slug, document_type_slug, documents)
357
+ check_async_response_format(response, :external_id => documents.first["external_id"], :status => "pending")
358
+ end
359
+ end
360
+
361
+ it 'returns false if a document cannot be created or updated due to an error' do
362
+ documents = [{:external_id => 'failed_doc', :fields => [{:type => :string, :name => :title}]}] # missing value
363
+
364
+ VCR.use_cassette(:async_create_or_update_document_failure) do
365
+ response = client.async_create_or_update_documents(engine_slug, document_type_slug, documents)
366
+ check_async_response_format(response, :external_id => documents.first["external_id"], :status => "failed", :errors => ["Missing required parameter: value"])
367
+ end
368
+ end
369
+ end
370
+
371
+ context '#document_receipts' do
372
+ before :each do
373
+ def get_receipt_ids
374
+ receipt_ids = nil
375
+ VCR.use_cassette(:async_create_or_update_document_success) do
376
+ response = client.async_create_or_update_documents(engine_slug, document_type_slug, documents)
377
+ receipt_ids = response["document_receipts"].map { |r| r["id"] }
378
+ end
379
+ receipt_ids
380
+ end
381
+ end
382
+
383
+ it 'returns array of hashes one for each receipt' do
384
+ VCR.use_cassette(:document_receipts_multiple) do
385
+ receipt_ids = get_receipt_ids
386
+ response = client.document_receipts(receipt_ids)
387
+ expect(response).to eq([{"id" => receipt_ids[0], "status" => "pending"}, {"id" => receipt_ids[1], "status" => "pending"}])
388
+ end
389
+ end
390
+ end
391
+
392
+ context '#index_documents' do
393
+ it 'returns #async_create_or_update_documents format return when async has been passed as true' do
394
+ VCR.use_cassette(:async_create_or_update_document_success) do
395
+ response = client.index_documents(engine_slug, document_type_slug, documents, :async => true)
396
+ check_async_response_format(response, :external_id => documents.first["external_id"], :status => "pending")
397
+ end
398
+ end
399
+
400
+ it 'returns document_receipts when successfull' do
401
+ VCR.use_cassette(:async_create_or_update_document_success) do
402
+ VCR.use_cassette(:document_receipts_multiple_complete) do
403
+ response = client.index_documents(engine_slug, document_type_slug, documents)
404
+ # Sort keys for Ruby 1.8.7 compatibility
405
+ expect(response.map(&:keys).map(&:sort)).to eq([["id", "link", "status"], ["id", "link", "status"]])
406
+ expect(response.map { |a| a["status"] }).to eq(["complete", "complete"])
407
+ end
408
+ end
409
+ end
410
+
411
+ it 'should timeout if the process takes longer than the timeout option passed' do
412
+ allow(client).to receive(:document_receipts) { sleep 0.05 }
413
+
414
+ VCR.use_cassette(:async_create_or_update_document_success) do
415
+ expect do
416
+ client.index_documents(engine_slug, document_type_slug, documents, :timeout => 0.01)
417
+ end.to raise_error(Timeout::Error)
418
+ end
419
+ end
420
+ end
421
+
422
+ context '#bulk_create_or_update_documents_verbose' do
423
+ it 'returns true for all documents successfully created or updated' do
424
+ VCR.use_cassette(:bulk_create_or_update_documents_verbose_success) do
425
+ response = client.create_or_update_documents_verbose(engine_slug, document_type_slug, documents)
426
+ expect(response).to eq([true, true])
427
+ end
428
+ end
429
+
430
+ it 'returns a descriptive error message if a document cannot be created or updated due to an error' do
431
+ documents = [{:external_id => 'failed_doc', :fields => [{:type => :string, :name => :title}]}] # missing value
432
+
433
+ VCR.use_cassette(:bulk_create_or_update_documents_verbose_failure) do
434
+ response = client.create_or_update_documents_verbose(engine_slug, document_type_slug, documents)
435
+ expect(response.size).to eq(1)
436
+ expect(response.first).to match /^Invalid field definition/
437
+ end
438
+ end
439
+ end
440
+
441
+ context '#update_document' do
442
+ it 'updates a document given its id and fields to update' do
443
+ fields = {:title => 'awesome new title', :channel_id => 'UC5VA5j05FjETg-iLekcyiBw'}
444
+ VCR.use_cassette(:update_document) do
445
+ response = client.update_document(engine_slug, document_type_slug, document_id, fields)
446
+ expect(response['external_id']).to eq(document_id)
447
+ expect(response['title']).to eq('awesome new title')
448
+ expect(response['channel_id']).to eq('UC5VA5j05FjETg-iLekcyiBw')
449
+ end
450
+ end
451
+
452
+ it 'raises an error if a unknown field is included' do
453
+ fields = {:not_a_field => 'not a field'}
454
+
455
+ VCR.use_cassette(:update_document_unknown_field_failure) do
456
+ expect do
457
+ response = client.update_document(engine_slug, document_type_slug, document_id, fields)
458
+ end.to raise_error
459
+ end
460
+ end
461
+ end
462
+
463
+ context "#update_documents" do
464
+ it 'returns true for each document successfully updated' do
465
+ documents = [{:external_id => 'INscMGmhmX4', :fields => {:title => 'hi'}}, {:external_id => 'XfY9Dsg_DZk', :fields => {:title => 'bye'}}]
466
+
467
+ VCR.use_cassette(:update_documents_success) do
468
+ response = client.update_documents(engine_slug, document_type_slug, documents)
469
+ expect(response).to eq([true, true])
470
+ end
471
+ end
472
+
473
+ it 'returns false if document is not successfully updated' do
474
+ documents = [{:external_id => 'not_there', :fields => [{:name => :title, :value => 'hi', :type => :string}]}]
475
+
476
+ VCR.use_cassette(:update_documents_failure_non_existent_document) do
477
+ response = client.update_documents(engine_slug, document_type_slug, documents)
478
+ expect(response).to eq([false])
479
+ end
480
+ end
481
+ end
482
+ end
483
+
484
+ context 'Analytics' do
485
+ let(:engine_slug) { 'recursion' }
486
+
487
+ context '#analytics_searches' do
488
+ it 'returns search counts for the default time frame' do
489
+ VCR.use_cassette(:analytics_searches) do
490
+ searches = client.analytics_searches(engine_slug)
491
+ expect(searches.size).to eq(15) # FIXME: is this a bug in the API?
492
+ expect(searches.first).to eq(['2013-09-13', 0])
493
+ end
494
+ end
495
+
496
+ it 'returns search counts for a specified time range' do
497
+ VCR.use_cassette(:analytics_searches_with_time_range) do
498
+ searches = client.analytics_searches(engine_slug, :start_date => '2013-01-01', :end_date => '2013-01-07')
499
+ expect(searches.size).to eq(7)
500
+ expect(searches.first).to eq(['2013-01-07', 0])
501
+ end
502
+ end
503
+
504
+ it 'returns search counts for a specified DocumentType' do
505
+ VCR.use_cassette(:analytics_searchs_with_document_type) do
506
+ searches = client.analytics_searches(engine_slug, :document_type_id => 'page')
507
+ expect(searches.size).to eq(15)
508
+ expect(searches.first).to eq(['2013-09-16', 0])
509
+ end
510
+ end
511
+
512
+ it 'returns search counts for a specified DocumentType and time range' do
513
+ VCR.use_cassette(:analytics_searches_with_document_type_and_time_range) do
514
+ searches = client.analytics_autoselects(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
515
+ expect(searches.size).to eq(7)
516
+ expect(searches.first).to eq(['2013-07-07', 0])
517
+ end
518
+ end
519
+ end
520
+
521
+ context '#analytics_autoselects' do
522
+ it 'returns autoselect counts for the default time frame' do
523
+ VCR.use_cassette(:analytics_autoselects) do
524
+ autoselects = client.analytics_autoselects(engine_slug)
525
+ expect(autoselects.size).to eq(15)
526
+ expect(autoselects.first).to eq(['2013-09-13', 0])
527
+ end
528
+ end
529
+
530
+ it 'returns autoselects counts for a specified time range' do
531
+ VCR.use_cassette(:analytics_autoselects_with_time_range) do
532
+ autoselects = client.analytics_autoselects(engine_slug, :start_date => '2013-07-01', :end_date => '2013-07-07')
533
+ expect(autoselects.size).to eq(7)
534
+ end
535
+ end
536
+
537
+ it 'returns autoselect counts for a specified DocumentType' do
538
+ VCR.use_cassette(:analytics_autoselects_with_document_type) do
539
+ autoselects = client.analytics_autoselects(engine_slug, :document_type_id => 'page')
540
+ expect(autoselects.size).to eq(15)
541
+ end
542
+ end
543
+
544
+ it 'returns autoselect counts for a specified DocumentType and time range' do
545
+ VCR.use_cassette(:analytics_autoselects_with_document_type_and_time_range) do
546
+ autoselects = client.analytics_autoselects(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
547
+ expect(autoselects.size).to eq(7)
548
+ end
549
+ end
550
+ end
551
+
552
+ context '#analytics_clicks' do
553
+ it 'returns click counts for the default time frame' do
554
+ VCR.use_cassette(:analytics_clicks) do
555
+ clicks = client.analytics_clicks(engine_slug)
556
+ expect(clicks.size).to eq(15)
557
+ expect(clicks.first).to eq(['2013-09-17', 0])
558
+ end
559
+ end
560
+
561
+ it 'returns clicks counts for a specified time range' do
562
+ VCR.use_cassette(:analytics_clicks_with_time_range) do
563
+ clicks = client.analytics_clicks(engine_slug, :start_date => '2013-07-01', :end_date => '2013-07-07')
564
+ expect(clicks.size).to eq(7)
565
+ expect(clicks.first).to eq(['2013-07-07', 0])
566
+ end
567
+ end
568
+
569
+ it 'returns click counts for a specified DocumentType' do
570
+ VCR.use_cassette(:analytics_clicks_with_document_type) do
571
+ clicks = client.analytics_clicks(engine_slug, :document_type_id => 'page')
572
+ expect(clicks.size).to eq(15)
573
+ end
574
+ end
575
+
576
+ it 'returns click counts for a specified DocumentType and time range' do
577
+ VCR.use_cassette(:analytics_clicks_with_document_type_and_time_range) do
578
+ clicks = client.analytics_clicks(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
579
+ expect(clicks.size).to eq(7)
580
+ expect(clicks.first).to eq(['2013-07-07', 0])
581
+ end
582
+ end
583
+ end
584
+
585
+ context '#analytics_top_queries' do
586
+ it 'returns top queries' do
587
+ VCR.use_cassette(:analytics_top_queries) do
588
+ top_queries = client.analytics_top_queries(engine_slug)
589
+ expect(top_queries.size).to eq(3)
590
+ expect(top_queries.first).to eq(['"fire watch"', 1])
591
+ end
592
+ end
593
+
594
+ it 'returns top queries with pagination' do
595
+ VCR.use_cassette(:analytics_top_queries_paginated) do
596
+ top_queries = client.analytics_top_queries(engine_slug, :start_date => '2013-08-01', :end_date => '2013-08-30', :per_page => 5, :page => 2)
597
+ expect(top_queries.size).to eq(5)
598
+ expect(top_queries.first).to eq(['no simple victory', 1])
599
+ end
600
+ end
601
+
602
+ it 'raises an error if the timeframe is to large' do
603
+ VCR.use_cassette(:analytics_top_queries_too_large) do
604
+ expect do
605
+ top_queries = client.analytics_top_queries(engine_slug, :start_date => '2013-01-01', :end_date => '2013-05-01')
606
+ end.to raise_error(Elastic::SiteSearch::BadRequest)
607
+ end
608
+ end
609
+ end
610
+
611
+ context 'analytics_top_no_result_queries' do
612
+ it 'returns top queries with no results for the default time range' do
613
+ VCR.use_cassette(:analytics_top_no_result_queries) do
614
+ top_no_result_queries = client.analytics_top_no_result_queries(engine_slug)
615
+ expect(top_no_result_queries.size).to eq(2)
616
+ expect(top_no_result_queries.first).to eq(['no results', 10])
617
+ end
618
+ end
619
+
620
+ it 'has top no result queries in date ranges' do
621
+ VCR.use_cassette(:analytics_top_no_result_queries_paginated) do
622
+ top_no_result_queries = client.analytics_top_no_result_queries(engine_slug, :start_date => '2013-08-01', :end_date => '2013-08-30', :per_page => 5, :page => 2)
623
+ expect(top_no_result_queries.size).to eq(1)
624
+ expect(top_no_result_queries.first).to eq(['no result again', 2])
625
+ end
626
+ end
627
+ end
628
+ end
629
+
630
+ context 'Domain' do
631
+ let(:engine_slug) { 'crawler-demo-site' }
632
+ let(:domain_id) { '51534c6e2ed960cc79000001' }
633
+
634
+ it 'gets all domains' do
635
+ VCR.use_cassette(:list_domains) do
636
+ domains = client.domains(engine_slug)
637
+ expect(domains.size).to eq(1)
638
+ expect(domains.first['id']).to eq(domain_id)
639
+ end
640
+ end
641
+
642
+ context '#domain' do
643
+ it 'shows a domain if it exists' do
644
+ VCR.use_cassette(:find_domain) do
645
+ domain = client.domain(engine_slug, domain_id)
646
+ expect(domain['id']).to eq(domain_id)
647
+ end
648
+ end
649
+
650
+ it 'raises an error if the domain does not exist' do
651
+ VCR.use_cassette(:find_domain_failure) do
652
+ expect do
653
+ domain = client.domain(engine_slug, 'bogus')
654
+ end.to raise_error(Elastic::SiteSearch::NonExistentRecord)
655
+ end
656
+ end
657
+ end
658
+
659
+ context '#create_domain' do
660
+ it 'creates a domain' do
661
+ VCR.use_cassette(:create_domain) do
662
+ url = 'http://www.zombo.com/'
663
+ domain = client.create_domain(engine_slug, url)
664
+ expect(domain['submitted_url']).to eq(url)
665
+ end
666
+ end
667
+ end
668
+
669
+ it 'destroys a domain' do
670
+ VCR.use_cassette(:destroy_domain) do
671
+ response = client.destroy_domain(engine_slug, '52324b132ed960589800004a')
672
+ expect(response).to be_nil
673
+ end
674
+ end
675
+
676
+ context '#recrawl_domain' do
677
+ it 'enqueues a request to recrawl a domain' do
678
+ VCR.use_cassette(:recrawl_domain_success) do
679
+ domain = client.recrawl_domain(engine_slug, domain_id)
680
+ expect(domain['id']).to eq(domain_id)
681
+ end
682
+ end
683
+
684
+ it 'raises an exception if domain recrawl is not allowed' do
685
+ VCR.use_cassette(:recrawl_domain_failure) do
686
+ expect do
687
+ domain = client.recrawl_domain(engine_slug, domain_id)
688
+ end.to raise_error(Elastic::SiteSearch::Forbidden)
689
+ end
690
+ end
691
+ end
692
+
693
+ context '#crawl_url' do
694
+ it 'enqueues a request to crawl a URL on a domain' do
695
+ VCR.use_cassette(:crawl_url) do
696
+ url = 'http://crawler-demo-site.herokuapp.com/2012/01/01/first-post.html'
697
+ crawled_url = client.crawl_url(engine_slug, domain_id, url)
698
+ expect(crawled_url['url']).to eq(url)
699
+ end
700
+ end
701
+ end
702
+ end
703
+
704
+ context 'Clickthrough' do
705
+ let(:query) { 'foo' }
706
+ let(:document_type_slug) { 'videos' }
707
+ let(:external_id) { 'FtX8nswnUKU'}
708
+
709
+ context "#log_clickthough" do
710
+ # Not thrilled with this test, but since nothing is returned all we
711
+ # can reasonably check is that an error isn't raised
712
+ it 'returns nil' do
713
+ VCR.use_cassette(:log_clickthrough_success) do
714
+ response = client.log_clickthrough(engine_slug, document_type_slug, query, external_id)
715
+ expect(response).to eq(nil)
716
+ end
717
+ end
718
+
719
+ it 'raises an error when missing params' do
720
+ VCR.use_cassette(:log_clickthrough_failure) do
721
+ expect do
722
+ client.log_clickthrough(engine_slug, document_type_slug, nil, external_id)
723
+ end.to raise_error(Elastic::SiteSearch::BadRequest)
724
+ end
725
+ end
726
+ end
727
+ end
728
+ end