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