diff --git a/Gemfile b/Gemfile
index 17883e5..6f49549 100644
--- a/Gemfile
+++ b/Gemfile
@@ -30,6 +30,8 @@ gem 'bootsnap', require: false
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platform: :mri
+ gem 'rspec', '~> 3.10.0'
+ gem 'rspec-rails', '~> 5.0.0'
end
group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index b581486..5530bf7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -49,6 +49,7 @@ GEM
byebug (11.1.3)
concurrent-ruby (1.1.9)
crass (1.0.6)
+ diff-lcs (1.4.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
erubi (1.10.0)
@@ -121,6 +122,27 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
+ rspec (3.10.0)
+ rspec-core (~> 3.10.0)
+ rspec-expectations (~> 3.10.0)
+ rspec-mocks (~> 3.10.0)
+ rspec-core (3.10.1)
+ rspec-support (~> 3.10.0)
+ rspec-expectations (3.10.1)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.10.0)
+ rspec-mocks (3.10.2)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.10.0)
+ rspec-rails (5.0.2)
+ actionpack (>= 5.2)
+ activesupport (>= 5.2)
+ railties (>= 5.2)
+ rspec-core (~> 3.10)
+ rspec-expectations (~> 3.10)
+ rspec-mocks (~> 3.10)
+ rspec-support (~> 3.10)
+ rspec-support (3.10.2)
ruby_dep (1.5.0)
spring (2.1.1)
spring-watcher-listen (2.0.1)
@@ -155,6 +177,8 @@ DEPENDENCIES
puma (~> 3.7)
rails (~> 5.2.0)
rest-client (>= 2.1.0.rc1, < 2.2)
+ rspec (~> 3.10.0)
+ rspec-rails (~> 5.0.0)
spring
spring-watcher-listen (~> 2.0.0)
sqlite3
diff --git a/README.md b/README.md
index 94cb16b..e8b2d13 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,7 @@ Apium is an API to access all public Center for Digital Research in the Humaniti
**[Apium Documentation](docs/README.md)**
This project is licensed under the terms of the [MIT license](LICENSE.md).
+
+## Run tests
+
+Run tests with `rails spec` or `rspec`
diff --git a/docs/adr/1_rspec_for_testing b/docs/adr/1_rspec_for_testing
new file mode 100644
index 0000000..0f79685
--- /dev/null
+++ b/docs/adr/1_rspec_for_testing
@@ -0,0 +1,40 @@
+# 1. Use rspec for testing
+
+Date: October 2021
+
+## Context
+
+Prior to October 2021, there were very few tests for the API checking functionality of the `SearchItemReq` and `SearchItemRes` classes written in minitest.
+
+## Decision
+
+We will use rspec for our tests
+
+### RSpec
+
+Pros:
+
+- `rspec-rails` imitates rails app file loading
+- describe / context / it statements provide self-documentation of features and behavior
+- well supported and used by many
+- extremely powerful mocking / stubbing and request functionality
+
+Cons:
+
+- fairly heavy application
+- documentation can be confusing
+
+### Minitest
+
+Pros:
+
+- very lightweight library
+- default for Rails
+
+Cons:
+
+- more basic in functionality
+
+## Consequences
+
+This change required us to migrate our existing tests from minitest to rspec. Additionally, this adds the dependency of rspec and several rspec-* libraries, rather than the built-in minitest. However, it will allow us to more thoroughly test the API in the future when we have time to think through and write tests!
diff --git a/lib/tasks/spec.rb b/lib/tasks/spec.rb
new file mode 100644
index 0000000..5bb6001
--- /dev/null
+++ b/lib/tasks/spec.rb
@@ -0,0 +1,9 @@
+begin
+ require 'rspec/core/rake_task'
+
+ RSpec::Core::RakeTask.new(:spec)
+
+ task :default => :spec
+rescue LoadError
+ # no rspec available
+end
diff --git a/test/fixtures/es_response.json b/spec/fixtures/files/es_response.json
similarity index 100%
rename from test/fixtures/es_response.json
rename to spec/fixtures/files/es_response.json
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 0000000..00345af
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,64 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('../config/environment', __dir__)
+# Prevent database truncation if the environment is production
+abort("The Rails environment is running in production mode!") if Rails.env.production?
+require 'rspec/rails'
+# Add additional requires below this line. Rails is not loaded until this point!
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+# run as spec files by default. This means that files in spec/support that end
+# in _spec.rb will both be required and run as specs, causing the specs to be
+# run twice. It is recommended that you do not name files matching this glob to
+# end with _spec.rb. You can configure this pattern with the --pattern
+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+#
+# The following line is provided for convenience purposes. It has the downside
+# of increasing the boot-up time by auto-requiring all files in the support
+# directory. Alternatively, in the individual `*_spec.rb` files, manually
+# require only the support files necessary.
+#
+# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
+
+# Checks for pending migrations and applies them before tests are run.
+# If you are not using ActiveRecord, you can remove these lines.
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ puts e.to_s.strip
+ exit 1
+end
+RSpec.configure do |config|
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # You can uncomment this line to turn off ActiveRecord support entirely.
+ # config.use_active_record = false
+
+ # RSpec Rails can automatically mix in different behaviours to your tests
+ # based on their file location, for example enabling you to call `get` and
+ # `post` in specs under `spec/controllers`.
+ #
+ # You can disable this behaviour by removing the line below, and instead
+ # explicitly tag your specs with their type, e.g.:
+ #
+ # RSpec.describe UsersController, type: :controller do
+ # # ...
+ # end
+ #
+ # The different available types are documented in the features, such as in
+ # https://relishapp.com/rspec/rspec-rails/docs
+ config.infer_spec_type_from_file_location!
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+ # arbitrary gems may also be filtered via:
+ # config.filter_gems_from_backtrace("gem name")
+end
diff --git a/spec/services/search_item_req_spec.rb b/spec/services/search_item_req_spec.rb
new file mode 100644
index 0000000..3dc29e2
--- /dev/null
+++ b/spec/services/search_item_req_spec.rb
@@ -0,0 +1,421 @@
+require "rails_helper"
+
+RSpec.describe SearchItemReq do
+ #
+ # ESCAPE_CHARS
+ #
+
+ describe "#escape_chars" do
+ it "escapes phrases with quotation marks" do
+ query = '"fire in the fireplace"'
+ expect(SearchItemReq.escape_chars(query)).to eq "\"fire in the fireplace\""
+ end
+
+ it "correctly handles (text:searches) queries" do
+ query = "(text:water) OR (annotations_text:Cather)"
+ expect(SearchItemReq.escape_chars(query)).to eq "(text:water) OR (annotations_text:Cather)"
+ end
+
+ it "does not escape ? character" do
+ expect(SearchItemReq.escape_chars("wat?r")).to eq "wat?r"
+ end
+
+ it "does not escape * character" do
+ expect(SearchItemReq.escape_chars("cat*")).to eq "cat*"
+ end
+
+ it "escapes quotation marks" do
+ expect(SearchItemReq.escape_chars('"something')).to eq "\"something"
+ expect(SearchItemReq.escape_chars('"phrase" plus "other phrase"'))
+ .to eq "\"phrase\" plus \"other phrase\""
+ end
+
+ it "escapes brackets and funky stuff" do
+ query = '{\\+~'
+ expect(SearchItemReq.escape_chars(query)).to eq "\\{\\\\\\+\\~"
+ end
+ end
+
+ #
+ # FACETS
+ #
+
+ describe "#facets" do
+ before do
+ @facets = SearchItemReq.new(config).facets
+ end
+
+ context "no overrides" do
+ let(:config) {{ "facet" => [ "title" ] }}
+ it "returns defaults" do
+ expect(@facets).to match({"title"=>{"terms"=>{"field"=>"title", "order"=>{"_count"=>"desc"}, "size"=>20}}})
+ end
+ end
+
+ context "some overrides" do
+ let(:config) {{
+ "facet_num" => 10,
+ "facet_sort" => "term|asc",
+ "facet" => [ "title", "subcategory" ]
+ }}
+ it "returns defaults and overridden values" do
+ expect(@facets).to match({"title"=>{"terms"=>{"field"=>"title", "order"=>{"_term"=>"asc"}, "size"=>10}}, "subcategory"=>{"terms"=>{"field"=>"subcategory", "order"=>{"_term"=>"asc"}, "size"=>10}}})
+ end
+ end
+
+ context "no facets provided" do
+ let(:config) {{
+ "facet_num" => 1,
+ "facet_sort" => "nonterm|asc",
+ "facet" => []
+ }}
+ it "is blank" do
+ expect(@facets).to eq({})
+ end
+ end
+
+ context "with dates" do
+ let(:config) {{"facet" => ["date.year", "date"]}}
+ it "returns date histogram" do
+ expect(@facets).to match({"date.year"=>{"date_histogram"=>{"field"=>"date", "interval"=>"year", "format"=>"yyyy", "min_doc_count"=>1, "order"=>{"_count"=>"desc"}}}, "date"=>{"date_histogram"=>{"field"=>"date", "interval"=>"day", "format"=>"yyyy-MM-dd", "min_doc_count"=>1, "order"=>{"_count"=>"desc"}}}})
+ end
+ end
+
+ context "with nested field" do
+ let(:config) {{"facet_sort" => "term|desc", "facet" => [ "creator.name" ]}}
+ it "returns nested aggregations" do
+ expect(@facets).to match({"creator.name"=>{"nested"=>{"path"=>"creator"}, "aggs"=>{"creator.name"=>{"terms"=>{"field"=>"creator.name", "order"=>{"_term"=>"desc"}, "size"=>20}}}}})
+ end
+ end
+
+ context "with a single facet" do
+ let(:config) {{ "facet" => "title" }}
+ it "returns facet field and defaults" do
+ expect(@facets).to match({"title"=>{"terms"=>{"field"=>"title", "order"=>{"_count"=>"desc"}, "size"=>20}}})
+ end
+ end
+
+ context "with term sorting using specified order" do
+ let(:config) {{ "facet" => ["title", "format"], "facet_sort" => "term|desc" }}
+ it "returns descending order by term" do
+ expect(@facets).to match({"title"=>{"terms"=>{"field"=>"title", "order"=>{"_term"=>"desc"}, "size"=>20}}, "format"=>{"terms"=>{"field"=>"format", "order"=>{"_term"=>"desc"}, "size"=>20}}})
+ end
+ end
+
+ context "with term sorting, no specified order" do
+ let(:config) {{ "facet" => ["title", "format"], "facet_sort" => "term" }}
+ it "returns descending order by term" do
+ expect(@facets).to match({"title"=>{"terms"=>{"field"=>"title", "order"=>{"_term"=>"desc"}, "size"=>20}}, "format"=>{"terms"=>{"field"=>"format", "order"=>{"_term"=>"desc"}, "size"=>20}}})
+ end
+ end
+
+ context "with sort count but no order" do
+ let(:config) {{ "facet" => ["title"], "facet_sort" => "count" }}
+ it "returns descending order by count" do
+ expect(@facets).to match({"title"=>{"terms"=>{"field"=>"title", "order"=>{"_count"=>"desc"}, "size"=>20}}})
+ end
+ end
+ end
+
+ #
+ # FILTERS
+ #
+
+ describe "#filters" do
+ before do
+ @filters = SearchItemReq.new(config).filters
+ end
+
+ context "single filter" do
+ let(:config) {{ "f" => ["category|Writings"] }}
+ it "returns single filter" do
+ expect(@filters).to match([{"term"=>{"category"=>"Writings"}}])
+ end
+ end
+
+ context "multiple filters" do
+ let(:config) {{ "f" => ["category|Writings", "author.name|Herriot, James"] }}
+ it "returns multiple filters, including nested" do
+ expect(@filters).to match([{"term"=>{"category"=>"Writings"}}, {"nested"=>{"path"=>"author", "query"=>{"term"=>{"author.name"=>"Herriot, James"}}}}])
+ end
+ end
+
+ context "multiple filters, including one with CR present" do
+ let(:config) {{ "f" => ["category|Writings", "places_written_k|Jaffrey, New Hampshire, United\r\n States"] }}
+ it "returns multiple filters with same characters provided" do
+ expect(@filters).to match([{"term"=>{"category"=>"Writings"}}, {"term"=>{"places_written_k"=>"Jaffrey, New Hampshire, United\n States"}}])
+ end
+ end
+
+ context "single year" do
+ let(:config) {{ "f" => ["date|1900"] }}
+ it "returns range from Jan 1 to Dec 31 of year" do
+ expect(@filters).to match([{"range"=>{"date"=>{"gte"=>"1900-01-01", "lte"=>"1900-12-31", "format"=>"yyyy-MM-dd"}}}])
+ end
+ end
+
+ context "double year" do
+ let(:config) {{ "f" => ["date|1900|1904"] }}
+ it "returns range from Jan 1 of first year to Dec 31 of last year" do
+ expect(@filters).to match([{"range"=>{"date"=>{"gte"=>"1900-01-01", "lte"=>"1904-12-31", "format"=>"yyyy-MM-dd"}}}])
+ end
+ end
+
+ context "double day range" do
+ let(:config) {{ "f" => ["date|1904-01-03|1908-12-10"] }}
+ it "returns range from and to dates provided" do
+ expect(@filters).to match([{"range"=>{"date"=>{"gte"=>"1904-01-03", "lte"=>"1908-12-10", "format"=>"yyyy-MM-dd"}}}])
+ end
+ end
+
+ context "nested field" do
+ let(:config) {{ "f" => ["creator.name|Willa, Cather"] }}
+ it "returns nested field" do
+ expect(@filters).to match([{"nested"=>{"path"=>"creator", "query"=>{"term"=>{"creator.name"=>"Willa, Cather"}}}}])
+ end
+ end
+
+ context "multiple filters, including a nested field with CR present" do
+ let(:config) {{ "f" => ["category|Writings", "author.name|Herriot,\r\nJames"] }}
+ it "returns multiple filters with same characters provided" do
+ expect(@filters).to match([{"term"=>{"category"=>"Writings"}}, {"nested"=>{"path"=>"author", "query"=>{"term"=>{"author.name"=>"Herriot,\nJames"}}}}])
+ end
+ end
+
+ context "dynamic field" do
+ let(:config) {{ "f" => ["publication_d|1900"] }}
+ it "returns dynamic date field with range for year" do
+ expect(@filters).to match([{"range"=>{"publication_d"=>{"gte"=>"1900-01-01", "lte"=>"1900-12-31", "format"=>"yyyy-MM-dd"}}}])
+ end
+ end
+
+ context "with non-array" do
+ let(:config) {{ "f" => "category|Writings" }}
+ it "returns single term filter" do
+ expect(@filters).to match([{"term"=>{"category"=>"Writings"}}])
+ end
+ end
+
+ context "where empty" do
+ let(:config) {{ "f" => "places|" }}
+ it "returns term filter for empty string" do
+ expect(@filters).to match(["term"=>{"places"=>""}])
+ end
+ end
+ end
+
+ #
+ # HIGHLIGHTS
+ #
+
+ describe "#highlights" do
+ before do
+ @highlights = SearchItemReq.new(config).highlights
+ end
+
+ context "no parameters" do
+ let(:config) {{}}
+ it "returns defaults" do
+ expect(@highlights).to match({"fields"=>{"text"=>{"fragment_size"=>100, "number_of_fragments"=>3}}})
+ end
+ end
+
+ context "specifies fragment size and number" do
+ let(:config) {{ "hl_chars" => 20, "hl_num" => 1 }}
+ it "returns correct settings" do
+ expect(@highlights).to match({"fields"=>{"text"=>{"fragment_size"=>20, "number_of_fragments"=>1}}})
+ end
+ end
+
+ context "sets highlight field and highlighting false" do
+ let(:config) {{ "hl_fl" => "annotations", "hl" => "false" }}
+ it "should not return any highlighting" do
+ expect(@highlights).to match({})
+ end
+ end
+
+ context "specifies highlight field list" do
+ let(:config) {{ "hl_fl" => "annotations, text" }}
+ it "should return default settings for the specified fields" do
+ expect(@highlights).to match({"fields"=>{"text"=>{"fragment_size"=>100, "number_of_fragments"=>3}, "annotations"=>{"fragment_size"=>100, "number_of_fragments"=>3}}})
+ end
+ end
+
+ context "specifies fragment size and numbers for multiple fields" do
+ let(:config) {{ "hl_chars" => 20, "hl_num" => 1, "hl_fl" => "annotations,extra" }}
+ it "sets the fragment size and number on each field" do
+ expect(@highlights).to match({"fields"=>{"text"=>{"fragment_size"=>20, "number_of_fragments"=>1}, "annotations"=>{"fragment_size"=>20, "number_of_fragments"=>1}, "extra"=>{"fragment_size"=>20, "number_of_fragments"=>1}}})
+ end
+ end
+ end
+
+ #
+ # SORT
+ #
+
+ describe "#sort" do
+ before do
+ @filters = SearchItemReq.new(config).sort
+ end
+
+ context "single sort" do
+ let(:config) {{ "sort" => ["title|asc"] }}
+ it "returns sort settings plus defaults" do
+ expect(@filters).to match([{"title"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}])
+ end
+ end
+
+ context "multiple sorts and subfield" do
+ let(:config) {{ "sort" => ["title|desc", "author.name|asc"] }}
+ it "returns requested sort fields in order with nested sorting" do
+ expect(@filters).to match([{"title"=>{"order"=>"desc", "mode"=>"max", "missing"=>"_last"}}, {"author.name"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last", "nested"=>{"path"=>"author"}}}])
+ end
+ end
+
+ context "with non-array" do
+ let(:config) {{ "sort" => "title|asc" }}
+ it "returns requested sort" do
+ expect(@filters).to match([{"title"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}])
+ end
+ end
+
+ context "no sort specified, query present" do
+ let(:config) {{ "q" => "water" }}
+ it "returns _score sorting" do
+ expect(@filters).to match(["_score"])
+ end
+ end
+
+ context "no sort direction specified, query present" do
+ let(:config) {{ "q" => "water", "sort" => "date" }}
+ it "returns field sorted by order ascending" do
+ expect(@filters).to match([{"date"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}])
+ end
+ end
+
+ context "sort specified, query present" do
+ let(:config) {{ "q" => "water", "sort" => "date|desc" }}
+ it "returns sort by field order descending" do
+ expect(@filters).to match([{"date"=>{"order"=>"desc", "mode"=>"max", "missing"=>"_last"}}])
+ end
+ end
+
+ context "no sort specified, no query" do
+ let(:config) {{}}
+ it "returns default sorting by identifier" do
+ expect(@filters).to match([{"identifier"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}])
+ end
+ end
+
+ context "no sort direction specified, no query" do
+ let(:config) {{ "sort" => "title" }}
+ it "returns sort by field ascending" do
+ expect(@filters).to match([{"title"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}])
+ end
+ end
+ end
+
+ #
+ # TEXT SEARCH
+ #
+
+ describe "#text_search" do
+ before do
+ @search = SearchItemReq.new(config).text_search
+ end
+
+ context "no text search" do
+ let(:config) {{}}
+ it "returns a match all clause" do
+ expect(@search).to match({ "match_all" => {}})
+ end
+ end
+
+ context "simple text query" do
+ let(:config) {{ "q" => "water" }}
+ it "returns a query string and default text field" do
+ expect(@search).to match({"query_string"=>{"default_field"=>"text", "query"=>"water"}})
+ end
+ end
+
+ context "with boolean" do
+ let(:config) {{ "q" => "water AND college" }}
+ it "returns boolean in query" do
+ expect(@search).to match({"query_string"=>{"default_field"=>"text", "query"=>"water AND college"}})
+ end
+ end
+
+ context "multiple text fields" do
+ let(:config) {{ "q" => "(text:water) AND (annotations:water)" }}
+ it "returns query string specifying multiple text fields" do
+ expect(@search).to match({"query_string"=>{"query"=>"(text:water) AND (annotations:water)"}})
+ end
+ end
+
+ context "multiple fields different input" do
+ let(:config) {{ "q" => "(text:water) OR (annotations:balcony)" }}
+ it "returns different queries for each text field" do
+ expect(@search).to match({"query_string"=>{"query"=>"(text:water) OR (annotations:balcony)"}})
+ end
+ end
+
+ context "multiple fields with grouped inputs" do
+ let(:config) {{ "q" => '(text:water OR "fire in the fireplace") OR (annotations:water AND "fire in the fireplace")'}}
+ it "returns " do
+ expect(@search).to match({"query_string"=>{"query"=>"(text:water OR \"fire in the fireplace\") OR (annotations:water AND \"fire in the fireplace\")"}})
+ end
+ end
+
+ context "non-text field search" do
+ let(:config) {{ "q" => "transcriptions_t:wouldnt" }}
+ it "returns query only for the non default text field specified" do
+ expect(@search).to match({"query_string"=>{"query"=>"transcriptions_t:wouldnt"}})
+ end
+ end
+
+ context "text field search with colon making it almost look like a text field search" do
+ let(:config) {{ "q" => "yosemite: cool place to visit" }}
+ it "returns query with the colon and everything" do
+ expect(@search).to match({"query_string"=>{"default_field"=>"text", "query"=>"yosemite: cool place to visit"}})
+ end
+ end
+
+ context "text field search with single quotation mark" do
+ let(:config) {{ "q" => "Exploring the Text: Cather's Hand" }}
+ it "returns query string with unescaped single quotation mark" do
+ expect(@search).to match({"query_string"=>{"default_field"=>"text", "query"=>"Exploring the Text: Cather's Hand"}})
+ end
+ end
+ end
+
+ #
+ # SOURCE
+ #
+
+ describe "#source" do
+ before do
+ @source = SearchItemReq.new(config).source
+ end
+
+ context "field list includes only" do
+ let(:config) {{ "fl" => "title, creator.name" }}
+ it "returns includes list" do
+ expect(@source).to match({"includes"=>["title", "creator.name"]})
+ end
+ end
+
+ context "field list excludes only" do
+ let(:config) {{ "fl" => "!title,!creator.name" }}
+ it "returns excludes list" do
+ expect(@source).to match({"excludes"=>["title", "creator.name"]})
+ end
+ end
+
+ context "field list both include and exclude fields with wildcards" do
+ let(:config) {{ "fl" => "id, title, date, !dat*" }}
+ it "returns list with both includes and excludes" do
+ expect(@source).to match({"includes"=>["id", "title", "date"], "excludes"=>["dat*"]})
+ end
+ end
+ end
+end
diff --git a/spec/services/search_item_res_spec.rb b/spec/services/search_item_res_spec.rb
new file mode 100644
index 0000000..01b2da3
--- /dev/null
+++ b/spec/services/search_item_res_spec.rb
@@ -0,0 +1,22 @@
+require "rails_helper"
+
+RSpec.describe SearchItemRes do
+ before do
+ file = file_fixture("es_response.json").read
+ @res = SearchItemRes.new(JSON.parse(file))
+ end
+
+ describe "#combine_highlights" do
+ it "smushes highlights into an array by field type" do
+ hl = @res.combine_highlights.dig(0, "highlight")
+ expect(hl).to match({"text"=>["View of the water from S. Lucia street in Naples. Napoli - Strada S. Lucia 35 Ediz Artistica RICTER"], "annotations"=>["This is a fake one that I made up to match water changes to highlighting"]})
+ end
+ end
+
+ describe "#reformat_facets" do
+ it "arranges nested fields and date fields for standard facet response across the board" do
+ facet = @res.reformat_facets
+ expect(facet).to match({"date.year"=>{"1896"=>4, "1920"=>4, "1934"=>4, "1908"=>3, "1938"=>3, "1942"=>3, "1916"=>2, "1929"=>2, "1933"=>2, "1936"=>2, "1941"=>2, "1899"=>1, "1905"=>1, "1909"=>1, "1911"=>1, "1917"=>1, "1918"=>1, "1925"=>1, "1930"=>1, "1931"=>1, "1935"=>1, "1940"=>1, "1944"=>1}, "person.name"=>{"Cather, Elsie"=>30, "Cather, Mary Virginia 'Jennie' Boak"=>22, "Cather, Roscoe"=>17, ""=>16, "Lewis, Edith"=>16, "Shannon, Margaret Cather"=>16, "Cather, Charles F."=>11, "Cather, Meta Schaper"=>8, "Gere, Mariel"=>8, "Auld, Jessica Cather"=>7, "Hambourg, Isabelle McClung"=>7, "Cather, Charles Douglass 'Douglass'"=>6, "Greenslet, Ferris"=>6, "Mellen, Mary Virginia Auld"=>6, "Sherwood, Carrie Miner"=>6, "Auld, William Thomas 'Tom, Will'"=>5, "Brockway, Virginia Cather"=>5, "Creighton, Mary Miner"=>5, "Hambourg, Jan"=>5, "Cather, James Donald"=>4}, "format"=>{"letter"=>50}})
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..ce33d66
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,96 @@
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+# The settings below are suggested to provide a good initial experience
+# with RSpec, but feel free to customize to your heart's content.
+=begin
+ # This allows you to limit a spec run to individual examples or groups
+ # you care about by tagging them with `:focus` metadata. When nothing
+ # is tagged with `:focus`, all examples get run. RSpec also provides
+ # aliases for `it`, `describe`, and `context` that include `:focus`
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ config.filter_run_when_matching :focus
+
+ # Allows RSpec to persist some state between runs in order to support
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # you configure your source control system to ignore this file.
+ config.example_status_persistence_file_path = "spec/examples.txt"
+
+ # Limits the available syntax to the non-monkey patched syntax that is
+ # recommended. For more details, see:
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
+ config.disable_monkey_patching!
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = "doc"
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+=end
+end
diff --git a/test/controllers/.keep b/test/controllers/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/fixtures/.keep b/test/fixtures/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/integration/.keep b/test/integration/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/mailers/.keep b/test/mailers/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/models/.keep b/test/models/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/services/search_item_req_test.rb b/test/services/search_item_req_test.rb
deleted file mode 100644
index 2a2c785..0000000
--- a/test/services/search_item_req_test.rb
+++ /dev/null
@@ -1,353 +0,0 @@
-require 'test_helper'
-
-class SearchItemReqTest < ActiveSupport::TestCase
-
- def test_escape_chars
-
- # phrase search (quotation marks)
- query = '"fire in the fireplace"'
- assert_equal "\"fire in the fireplace\"", SearchItemReq.escape_chars(query)
-
- # make sure that (text:searches) are not destroyed
- query = '(text:water) OR (annotations_text:Cather)'
- assert_equal "(text:water) OR (annotations_text:Cather)", SearchItemReq.escape_chars(query)
-
- # do not escape ? and *
- query = 'wat?r OR cat*'
- assert_equal "wat?r OR cat*", SearchItemReq.escape_chars(query)
-
- # escape odd numbered quotation marks
- query = '"something'
- assert_equal "\"something", SearchItemReq.escape_chars(query)
- query = '"phrase" plus "'
- assert_equal "\"phrase\" plus \"", SearchItemReq.escape_chars(query)
-
- # escape brackets, etc
- query = '{\\+~'
- assert_equal "\\{\\\\\\+\\~", SearchItemReq.escape_chars(query)
-
- end
-
- def test_facets
-
- # normal with no pagination overrides
- facets = SearchItemReq.new({ "facet" => [ "title" ] }).facets
- assert_equal(
- {"title"=>{"terms"=>{"field"=>"title", "order"=>{"_count"=>"desc"}, "size"=>20}}},
- facets
- )
-
- # normal with pagination overrides, multiple facets
- facets = SearchItemReq.new({
- "facet_num" => 10,
- "facet_sort" => "term|asc",
- "facet" => [ "title", "subcategory" ]
- }).facets
- assert_equal(
- {"title"=>{"terms"=>{"field"=>"title", "order"=>{"_term"=>"asc"}, "size"=>10}}, "subcategory"=>{"terms"=>{"field"=>"subcategory", "order"=>{"_term"=>"asc"}, "size"=>10}}},
- facets
- )
-
- # should be blank if there are no facets provided
- facets = SearchItemReq.new({
- "facet_num" => 1,
- "facet_sort" => "nonterm|asc",
- "facet" => []
- }).facets
- assert_equal({}, facets)
-
- # getting dates involved
- facets = SearchItemReq.new({ "facet" => [ "date.year", "date"] }).facets
- assert_equal(
- {"date.year"=>{"date_histogram"=>{"field"=>"date", "interval"=>"year", "format"=>"yyyy", "min_doc_count"=>1, "order"=>{"_count"=>"desc"}}}, "date"=>{"date_histogram"=>{"field"=>"date", "interval"=>"day", "format"=>"yyyy-MM-dd", "min_doc_count"=>1, "order"=>{"_count"=>"desc"}}}},
- facets
- )
-
- # nested field
- facets = SearchItemReq.new({
- "facet_sort" => "term|desc",
- "facet" => [ "creator.name" ]
- }).facets
- assert_equal(
- {"creator.name"=>{"nested"=>{"path"=>"creator"}, "aggs"=>{"creator.name"=>{"terms"=>{"field"=>"creator.name", "order"=>{"_term"=>"desc"}, "size"=>20}}}}},
- facets
- )
-
- # with non-array
- facets = SearchItemReq.new({ "facet" => "title" }).facets
- assert_equal(
- {"title"=>{"terms"=>{"field"=>"title", "order"=>{"_count"=>"desc"}, "size"=>20}}},
- facets
- )
-
- # sort term order specified
- facets = SearchItemReq.new({ "facet" => ["title", "format"], "facet_sort" => "term|desc" }).facets
- assert_equal(
- {"title"=>{"terms"=>{"field"=>"title", "order"=>{"_term"=>"desc"}, "size"=>20}}, "format"=>{"terms"=>{"field"=>"format", "order"=>{"_term"=>"desc"}, "size"=>20}}},
- facets
- )
-
- # sort term no order specified
- facets = SearchItemReq.new({ "facet" => ["title", "format"], "facet_sort" => "term" }).facets
- assert_equal(
- {"title"=>{"terms"=>{"field"=>"title", "order"=>{"_term"=>"desc"}, "size"=>20}}, "format"=>{"terms"=>{"field"=>"format", "order"=>{"_term"=>"desc"}, "size"=>20}}},
- facets
- )
-
- # sort count, no order specified
- facets = SearchItemReq.new({ "facet" => ["title"], "facet_sort" => "count" }).facets
- assert_equal(
- {"title"=>{"terms"=>{"field"=>"title", "order"=>{"_count"=>"desc"}, "size"=>20}}},
- facets
- )
-
- end
-
- def test_filters
-
- # single filter
- filters = SearchItemReq.new({ "f" => ["category|Writings"] }).filters
- assert_equal(
- [{"term"=>{"category"=>"Writings"}}],
- filters
- )
-
- # multiple filters
- filters = SearchItemReq.new({ "f" => ["category|Writings", "author.name|Herriot, James"] }).filters
- assert_equal(
- [{"term"=>{"category"=>"Writings"}}, {"nested"=>{"path"=>"author", "query"=>{"term"=>{"author.name"=>"Herriot, James"}}}}],
- filters
- )
-
- # multiple filters, including one with CR present
- filters = SearchItemReq.new({ "f" => ["category|Writings", "places_written_k|Jaffrey, New Hampshire, United\r\n States"] }).filters
- assert_equal(
- [{"term"=>{"category"=>"Writings"}}, {"term"=>{"places_written_k"=>"Jaffrey, New Hampshire, United\n States"}}],
- filters
- )
-
- # single year
- filters = SearchItemReq.new({ "f" => ["date|1900"] }).filters
- assert_equal(
- [{"range"=>{"date"=>{"gte"=>"1900-01-01", "lte"=>"1900-12-31", "format"=>"yyyy-MM-dd"}}}],
- filters
- )
-
- # double year
- filters = SearchItemReq.new({ "f" => ["date|1900|1904"] }).filters
- assert_equal(
- [{"range"=>{"date"=>{"gte"=>"1900-01-01", "lte"=>"1904-12-31", "format"=>"yyyy-MM-dd"}}}],
- filters
- )
-
- # double day range
- filters = SearchItemReq.new({ "f" => ["date|1904-01-03|1908-12-10"] }).filters
- assert_equal(
- [{"range"=>{"date"=>{"gte"=>"1904-01-03", "lte"=>"1908-12-10", "format"=>"yyyy-MM-dd"}}}],
- filters
- )
-
- # nested field
- filters = SearchItemReq.new({ "f" => ["creator.name|Willa, Cather"] }).filters
- assert_equal(
- [{"nested"=>{"path"=>"creator", "query"=>{"term"=>{"creator.name"=>"Willa, Cather"}}}}],
- filters
- )
-
- # multiple filters, including a nested field with CR present
- filters = SearchItemReq.new({ "f" => ["category|Writings", "author.name|Herriot,\r\nJames"] }).filters
- assert_equal(
- [{"term"=>{"category"=>"Writings"}}, {"nested"=>{"path"=>"author", "query"=>{"term"=>{"author.name"=>"Herriot,\nJames"}}}}],
- filters
- )
-
- # dynamic field
- filters = SearchItemReq.new({ "f" => ["publication_d|1900"] }).filters
- assert_equal(
- [{"range"=>{"publication_d"=>{"gte"=>"1900-01-01", "lte"=>"1900-12-31", "format"=>"yyyy-MM-dd"}}}],
- filters
- )
-
- # with non-array
- filters = SearchItemReq.new({ "f" => "category|Writings" }).filters
- assert_equal([{"term"=>{"category"=>"Writings"}}], filters)
-
- # where empty
- filters = SearchItemReq.new({ "f" => "places|" }).filters
- assert_equal(["term"=>{"places"=>""}], filters)
-
- end
-
- def test_highlights
-
- # no parameters
- hl = SearchItemReq.new({}).highlights
- assert_equal(
- {"fields"=>{"text"=>{"fragment_size"=>100, "number_of_fragments"=>3}}},
- hl
- )
-
- # specifying fragment size and number
- hl = SearchItemReq.new({ "hl_chars" => 20, "hl_num" => 1 }).highlights
- assert_equal(
- {"fields"=>{"text"=>{"fragment_size"=>20, "number_of_fragments"=>1}}},
- hl
- )
-
- # fragment size and number multiple fields
- hl = SearchItemReq.new({ "hl_chars" => 20, "hl_num" => 1, "hl_fl" => "annotations,extra" }).highlights
- assert_equal(
- {"fields"=>{"text"=>{"fragment_size"=>20, "number_of_fragments"=>1}, "annotations"=>{"fragment_size"=>20, "number_of_fragments"=>1}, "extra"=>{"fragment_size"=>20, "number_of_fragments"=>1}}},
- hl
- )
-
- # no highlights despite params
- hl = SearchItemReq.new({ "hl_fl" => "annotations", "hl" => "false" }).highlights
- assert_equal({}, hl)
-
- # highlight field list
- hl = SearchItemReq.new({ "hl_fl" => "annotations, text" }).highlights
- assert_equal(
- {"fields"=>{"text"=>{"fragment_size"=>100, "number_of_fragments"=>3}, "annotations"=>{"fragment_size"=>100, "number_of_fragments"=>3}}},
- hl
- )
-
- end
-
- def test_sort
-
- # single sort
- sort = SearchItemReq.new({ "sort" => ["title|asc"] }).sort
- assert_equal(
- [{"title"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}],
- sort
- )
-
- # multiple sorts and subfield
- sort = SearchItemReq.new({ "sort" => ["title|desc", "author.name|asc"] }).sort
- assert_equal(
- [{"title"=>{"order"=>"desc", "mode"=>"max", "missing"=>"_last"}}, {"author.name"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last", "nested"=>{"path"=>"author"}}}],
- sort
- )
-
- # with non-array
- sort = SearchItemReq.new({ "sort" => "title|asc" }).sort
- assert_equal(
- [{"title"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}],
- sort
- )
-
- # no sort specified, query present
- sort = SearchItemReq.new({ "q" => "water" }).sort
- assert_equal(["_score"], sort)
-
- # no sort direction specified, query present
- sort = SearchItemReq.new({ "q" => "water", "sort" => "date" }).sort
- assert_equal(
- [{"date"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}],
- sort
- )
-
- # sort specified, query present
- sort = SearchItemReq.new({ "q" => "water", "sort" => "date|desc" }).sort
- assert_equal(
- [{"date"=>{"order"=>"desc", "mode"=>"max", "missing"=>"_last"}}],
- sort
- )
-
- # no sort specified, no query
- sort = SearchItemReq.new({}).sort
- assert_equal(
- [{"identifier"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}],
- sort
- )
-
- # no sort direction specified, no query
- sort = SearchItemReq.new({ "sort" => "title" }).sort
- assert_equal(
- [{"title"=>{"order"=>"asc", "mode"=>"min", "missing"=>"_last"}}],
- sort
- )
- end
-
- def test_text_search
-
- # simple
- text = SearchItemReq.new({ "q" => "water" }).text_search
- assert_equal(
- {"query_string"=>{"default_field"=>"text", "query"=>"water"}},
- text
- )
-
- # boolean
- text = SearchItemReq.new({ "q" => "water AND college" }).text_search
- assert_equal(
- {"query_string"=>{"default_field"=>"text", "query"=>"water AND college"}},
- text
- )
-
- # multiple fields
- text = SearchItemReq.new({ "q" => "(text:water) AND (annotations:water)" }).text_search
- assert_equal(
- {"query_string"=>{"query"=>"(text:water) AND (annotations:water)"}},
- text
- )
-
- # multiple fields different input
- text = SearchItemReq.new({ "q" => "(text:water) OR (annotations:balcony)" }).text_search
- assert_equal(
- {"query_string"=>{"query"=>"(text:water) OR (annotations:balcony)"}},
- text
- )
-
- # multiple fields with grouped inputs
- text = SearchItemReq.new({ "q" => '(text:water OR "fire in the fireplace") OR (annotations:water AND "fire in the fireplace")'}).text_search
- assert_equal(
- {"query_string"=>{"query"=>"(text:water OR \"fire in the fireplace\") OR (annotations:water AND \"fire in the fireplace\")"}},
- text
- )
-
- # non-text field search
- text = SearchItemReq.new({ "q" => "transcriptions_t:wouldnt" }).text_search
- assert_equal(
- {"query_string"=>{"query"=>"transcriptions_t:wouldnt"}},
- text
- )
-
- # text field search beginning with what looks like text field
- text = SearchItemReq.new({ "q" => "yosemite: cool place to visit" }).text_search
- assert_equal(
- {"query_string"=>{"default_field"=>"text", "query"=>"yosemite: cool place to visit"}},
- text
- )
-
- # text field search beginning with what really looks like a text field
- text = SearchItemReq.new({ "q" => "Exploring the Text: Cather's Hand" }).text_search
- assert_equal(
- {"query_string"=>{"default_field"=>"text", "query"=>"Exploring the Text: Cather's Hand"}},
- text
- )
-
- # none
- text = SearchItemReq.new({}).text_search
- assert_equal({ "match_all" => {} }, text)
-
- end
-
- def test_source
-
- # spaces, whitelist only
- source = SearchItemReq.new({ "fl" => "title, creator.name" }).source
- assert_equal({"includes"=>["title", "creator.name"]}, source)
-
- # blacklist only
- source = SearchItemReq.new({ "fl" => "!title,!creator.name" }).source
- assert_equal({"excludes"=>["title", "creator.name"]}, source)
-
- # both
- source = SearchItemReq.new({ "fl" => "id, title, date, !dat*" }).source
- assert_equal({"includes"=>["id", "title", "date"], "excludes"=>["dat*"]}, source)
-
- end
-
-end
diff --git a/test/services/search_item_res_test.rb b/test/services/search_item_res_test.rb
deleted file mode 100644
index e435b5b..0000000
--- a/test/services/search_item_res_test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'test_helper'
-
-class SearchItemResTest < ActiveSupport::TestCase
-
- def setup
- # test json generated with
- # items?facet[]=person.name&facet[]=format&facet[]=date.year&debug=true&q=water
- this_dir = File.dirname(__FILE__)
- file = File.read("#{this_dir}/../fixtures/es_response.json")
- @es = JSON.parse(file)
- end
-
- def test_combine_highlights
- hl = SearchItemRes.new(@es).combine_highlights
- first = hl.dig(0, "highlight")
- assert_equal first, {"text"=>["View of the water from S. Lucia street in Naples. Napoli - Strada S. Lucia 35 Ediz Artistica RICTER"], "annotations"=>["This is a fake one that I made up to match water changes to highlighting"]}
- end
-
- def test_reformat_facets
- facets = SearchItemRes.new(@es).reformat_facets
- assert_equal facets, {"date.year"=>{"1896"=>4, "1920"=>4, "1934"=>4, "1908"=>3, "1938"=>3, "1942"=>3, "1916"=>2, "1929"=>2, "1933"=>2, "1936"=>2, "1941"=>2, "1899"=>1, "1905"=>1, "1909"=>1, "1911"=>1, "1917"=>1, "1918"=>1, "1925"=>1, "1930"=>1, "1931"=>1, "1935"=>1, "1940"=>1, "1944"=>1}, "person.name"=>{"Cather, Elsie"=>30, "Cather, Mary Virginia 'Jennie' Boak"=>22, "Cather, Roscoe"=>17, ""=>16, "Lewis, Edith"=>16, "Shannon, Margaret Cather"=>16, "Cather, Charles F."=>11, "Cather, Meta Schaper"=>8, "Gere, Mariel"=>8, "Auld, Jessica Cather"=>7, "Hambourg, Isabelle McClung"=>7, "Cather, Charles Douglass 'Douglass'"=>6, "Greenslet, Ferris"=>6, "Mellen, Mary Virginia Auld"=>6, "Sherwood, Carrie Miner"=>6, "Auld, William Thomas 'Tom, Will'"=>5, "Brockway, Virginia Cather"=>5, "Creighton, Mary Miner"=>5, "Hambourg, Jan"=>5, "Cather, James Donald"=>4}, "format"=>{"letter"=>50}}
- end
-
-end
diff --git a/test/test_helper.rb b/test/test_helper.rb
deleted file mode 100644
index 92e39b2..0000000
--- a/test/test_helper.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-ENV['RAILS_ENV'] ||= 'test'
-require File.expand_path('../../config/environment', __FILE__)
-require 'rails/test_help'
-
-class ActiveSupport::TestCase
- # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
- fixtures :all
-
- # Add more helper methods to be used by all tests here...
-end