diff --git a/Dockerfile b/Dockerfile index dd17508c..69885a82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.3 # Debian releases: # -FROM ruby:3.0.2-slim-buster +FROM ruby:3.1.0-slim-bullseye ARG app_name=baw-server ARG app_user=baw_web ARG version= diff --git a/Gemfile b/Gemfile index c62332aa..8f02f171 100644 --- a/Gemfile +++ b/Gemfile @@ -25,8 +25,8 @@ gem 'bootsnap', require: false # standardised way to validate objects gem 'dry-monads' gem 'dry-struct' -gem 'dry-validation' gem 'dry-transformer' +gem 'dry-validation' # Async/promises/futures gem 'concurrent-ruby', '~> 1', require: 'concurrent' @@ -37,8 +37,8 @@ gem 'faraday' gem 'faraday_middleware' # api docs -gem 'rswag-api' -gem 'rswag-ui' +gem 'rswag-api', github: 'rswag/rswag', glob: 'rswag-api/*.gemspec' +gem 'rswag-ui', github: 'rswag/rswag', glob: 'rswag-ui/*.gemspec' # uri parsing and generation gem 'addressable' @@ -50,8 +50,8 @@ gem 'deep_sort' # DO NOT change rails version without also changing composite_primary_keys version # https://github.com/composite-primary-keys/composite_primary_keys -RAILS_VERSION = '~> 6.1.4' -COMPOSITE_PRIMARY_KEYS_VERSION = '~> 13' +RAILS_VERSION = '~> 7.0.1' +COMPOSITE_PRIMARY_KEYS_VERSION = '~> 14' group :server do # RAILS @@ -59,7 +59,6 @@ group :server do # ------------------------------------- gem 'rack-cors', '~> 1.1.1', require: 'rack/cors' gem 'rails', RAILS_VERSION - gem 'responders', '~> 3.0.1' # deal with chrome and same site cookies gem 'rails_same_site_cookie' @@ -107,7 +106,7 @@ group :server do # https://github.com/plataformatec/devise/blob/master/CHANGELOG.md # http://joanswork.com/devise-3-1-update/ gem 'cancancan', '> 3' - gem 'devise', '~> 4.7.0' + gem 'devise', '~> 4.8.1' gem 'devise-i18n' gem 'role_model', '~> 0.8.1' # Use ActiveModel has_secure_password @@ -124,7 +123,7 @@ group :server do # extensions to arel https://github.com/Faveod/arel-extensions # in particular, we use `cast`, and `coalesce` - gem 'arel_extensions' + gem 'arel_extensions', '>= 2.1.0' # as the name says gem 'composite_primary_keys', COMPOSITE_PRIMARY_KEYS_VERSION @@ -183,7 +182,7 @@ group :workers, :server do # logging gem 'amazing_print' - gem 'rails_semantic_logger' + gem 'rails_semantic_logger', '>= 4.10.0' # SETTINGS # ------------------------------------- @@ -208,7 +207,7 @@ end group :development do # allow debugging #gem 'debase', '>= 0.2.5.beta2' - gem 'debug', ">= 1.0.0" + gem 'debug', '>= 1.0.0' gem 'readapt' #gem 'ruby-debug-ide', '>= 0.7.2' #gem 'traceroute' @@ -216,6 +215,10 @@ group :development do # a ruby language server gem 'solargraph' gem 'solargraph-rails', '>= 0.2.2.pre.2' + + # official ruby typing support + gem 'typeprof' + # needed by bundler/soalrgraph for language server? gem 'actionview', RAILS_VERSION @@ -285,7 +288,7 @@ group :test do gem 'zonebie' # api docs - gem 'rswag-specs' + gem 'rswag-specs', github: 'rswag/rswag', glob: 'rswag-specs/*.gemspec' # old docs (deprecated) gem 'rspec_api_documentation', '~> 4.8.0' diff --git a/Gemfile.lock b/Gemfile.lock index ea2a8abb..da3c23e9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,15 +1,42 @@ +GIT + remote: https://github.com/rswag/rswag.git + revision: b971f299c12f87cd2ca579d7c95d1ce942b767cc + glob: rswag-api/*.gemspec + specs: + rswag-api (0.0.0) + railties (>= 3.1, < 7.1) + +GIT + remote: https://github.com/rswag/rswag.git + revision: b971f299c12f87cd2ca579d7c95d1ce942b767cc + glob: rswag-specs/*.gemspec + specs: + rswag-specs (0.0.0) + activesupport (>= 3.1, < 7.1) + json-schema (~> 2.2) + railties (>= 3.1, < 7.1) + +GIT + remote: https://github.com/rswag/rswag.git + revision: b971f299c12f87cd2ca579d7c95d1ce942b767cc + glob: rswag-ui/*.gemspec + specs: + rswag-ui (0.0.0) + actionpack (>= 3.1, < 7.1) + railties (>= 3.1, < 7.1) + GIT remote: https://github.com/socketry/async - revision: cec74b739fe50d44b35f919381740cdc118f5cdf + revision: 456df488d801572821eaf5ec2fda10e3b9744a5f specs: async (2.0.0) console (~> 1.10) - event (~> 1.0) + io-event (~> 1.0.0) timers (~> 4.1) GIT remote: https://github.com/socketry/async-http - revision: 07634ec6098648e1786d2ae5a1b6f871aa5e2f17 + revision: bbe0f75c5da22d0534f24ff96deb7e9fb761cbe6 specs: async-http (0.56.5) async (>= 1.25) @@ -18,6 +45,7 @@ GIT protocol-http (~> 0.22.0) protocol-http1 (~> 0.14.0) protocol-http2 (~> 0.14.0) + traces (~> 0.4.0) GIT remote: https://github.com/socketry/falcon @@ -41,40 +69,47 @@ GEM specs: aasm (5.2.0) concurrent-ruby (~> 1.0) - actioncable (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + actioncable (7.0.1) + actionpack (= 7.0.1) + activesupport (= 7.0.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionmailbox (7.0.1) + actionpack (= 7.0.1) + activejob (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) mail (>= 2.7.1) - actionmailer (6.1.4.1) - actionpack (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activesupport (= 6.1.4.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.1) + actionpack (= 7.0.1) + actionview (= 7.0.1) + activejob (= 7.0.1) + activesupport (= 7.0.1) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (6.1.4.1) - actionview (= 6.1.4.1) - activesupport (= 6.1.4.1) - rack (~> 2.0, >= 2.0.9) + actionpack (7.0.1) + actionview (= 7.0.1) + activesupport (= 7.0.1) + rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.1) - actionpack (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actiontext (7.0.1) + actionpack (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.4.1) - activesupport (= 6.1.4.1) + actionview (7.0.1) + activesupport (= 7.0.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -82,39 +117,41 @@ GEM active_link_to (1.0.5) actionpack addressable - active_storage_validations (0.9.5) - rails (>= 5.2.0) - activejob (6.1.4.1) - activesupport (= 6.1.4.1) + active_storage_validations (0.9.6) + activejob (>= 5.2.0) + activemodel (>= 5.2.0) + activestorage (>= 5.2.0) + activesupport (>= 5.2.0) + activejob (7.0.1) + activesupport (= 7.0.1) globalid (>= 0.3.6) - activemodel (6.1.4.1) - activesupport (= 6.1.4.1) - activerecord (6.1.4.1) - activemodel (= 6.1.4.1) - activesupport (= 6.1.4.1) - activestorage (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activesupport (= 6.1.4.1) - marcel (~> 1.0.0) + activemodel (7.0.1) + activesupport (= 7.0.1) + activerecord (7.0.1) + activemodel (= 7.0.1) + activesupport (= 7.0.1) + activestorage (7.0.1) + actionpack (= 7.0.1) + activejob (= 7.0.1) + activerecord (= 7.0.1) + activesupport (= 7.0.1) + marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.1) + activesupport (7.0.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) - acts_as_paranoid (0.7.3) - activerecord (>= 5.2, < 7.0) - activesupport (>= 5.2, < 7.0) + acts_as_paranoid (0.8.0) + activerecord (>= 5.2, < 7.1) + activesupport (>= 5.2, < 7.1) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - amazing_print (1.3.0) - annotate (3.1.1) - activerecord (>= 3.2, < 7.0) - rake (>= 10.4, < 14.0) - arel_extensions (2.0.22) + amazing_print (1.4.0) + annotate (2.6.5) + activerecord (>= 2.3.0) + rake (>= 0.8.7) + arel_extensions (2.1.0) activerecord (>= 6.0) ast (2.4.2) async-container (0.16.12) @@ -129,8 +166,8 @@ GEM async async-pool (0.3.9) async (>= 1.25) - attr_extras (6.2.4) - autoprefixer-rails (10.3.1.0) + attr_extras (6.2.5) + autoprefixer-rails (10.4.2.0) execjs (~> 2) backport (1.2.0) bcrypt (3.1.16) @@ -138,14 +175,14 @@ GEM benchmark-malloc (0.2.0) benchmark-perf (0.6.0) benchmark-trend (0.4.0) - bootsnap (1.8.1) - msgpack (~> 1.0) + bootsnap (1.10.2) + msgpack (~> 1.2) bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) build-environment (1.13.0) builder (3.2.4) - bullet (6.1.5) + bullet (7.0.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) cancancan (3.3.0) @@ -164,15 +201,15 @@ GEM sassc-rails (>= 2.0.0) comfy_bootstrap_form (4.0.9) rails (>= 5.0.0) - composite_primary_keys (13.0.0) - activerecord (~> 6.1.0) + composite_primary_keys (14.0.3) + activerecord (~> 7.0.0) concurrent-ruby (1.1.9) concurrent-ruby-edge (0.6.0) concurrent-ruby (~> 1.1.6) - config (3.1.0) + config (3.1.1) deep_merge (~> 1.2, >= 1.2.1) dry-validation (~> 1.0, >= 1.0.0) - console (1.13.1) + console (1.14.0) fiber-local coveralls (0.8.23) json (>= 1.8, < 3) @@ -194,45 +231,45 @@ GEM database_cleaner-redis (2.0.0) database_cleaner-core (~> 2.0.0) redis - debug (1.3.4) + debug (1.4.0) irb (>= 1.3.6) reline (>= 0.2.7) - deep_merge (1.2.1) + deep_merge (1.2.2) deep_sort (0.1.6) descriptive-statistics (2.2.0) - devise (4.7.3) + devise (4.8.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.9.4) - devise (>= 4.7.1) - diff-lcs (1.4.4) + devise-i18n (1.10.1) + devise (>= 4.8.0) + diff-lcs (1.5.0) + digest (3.1.0) docile (1.4.0) - dotiw (5.3.1) + dotiw (5.3.2) activesupport i18n - dry-configurable (0.12.1) + dry-configurable (0.14.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.5, >= 0.5.0) - dry-container (0.8.0) + dry-core (~> 0.6) + dry-container (0.9.0) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1, >= 0.1.3) + dry-configurable (~> 0.13, >= 0.13.0) dry-core (0.7.1) concurrent-ruby (~> 1.0) - dry-equalizer (0.3.0) dry-inflector (0.2.1) - dry-initializer (3.0.4) + dry-initializer (3.1.1) dry-logic (1.2.0) concurrent-ruby (~> 1.0) dry-core (~> 0.5, >= 0.5) dry-monads (1.4.0) concurrent-ruby (~> 1.0) dry-core (~> 0.7) - dry-schema (1.7.1) + dry-schema (1.8.0) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.8, >= 0.8.3) + dry-configurable (~> 0.13, >= 0.13.0) dry-core (~> 0.5, >= 0.5) dry-initializer (~> 3.0) dry-logic (~> 1.0) @@ -248,26 +285,24 @@ GEM dry-core (~> 0.5, >= 0.5) dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 1.0, >= 1.0.2) - dry-validation (1.6.0) + dry-validation (1.7.0) concurrent-ruby (~> 1.0) dry-container (~> 0.7, >= 0.7.1) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) + dry-core (~> 0.5, >= 0.5) dry-initializer (~> 3.0) - dry-schema (~> 1.5, >= 1.5.2) + dry-schema (~> 1.8, >= 1.8.0) e2mmap (0.1.0) - enumerize (2.4.0) + enumerize (2.5.0) activesupport (>= 3.2) erubi (1.10.0) erubis (2.7.0) - et-orbi (1.2.4) + et-orbi (1.2.6) tzinfo - ethon (0.14.0) + ethon (0.15.0) ffi (>= 1.15.0) - event (1.0.3) - exception_notification (4.4.3) - actionmailer (>= 4.0, < 7) - activesupport (>= 4.0, < 7) + exception_notification (4.5.0) + actionmailer (>= 5.2, < 8) + activesupport (>= 5.2, < 8) execjs (2.8.1) factory_bot (6.2.0) activesupport (>= 5.0.0) @@ -276,16 +311,17 @@ GEM railties (>= 5.0.0) faker (2.19.0) i18n (>= 1.6, < 2) - faraday (1.7.1) + faraday (1.9.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) faraday-em_http (1.0.0) faraday-em_synchrony (1.0.0) @@ -293,17 +329,20 @@ GEM faraday-http-cache (2.2.0) faraday (>= 0.8) faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) faraday (~> 1.0) - ffi (1.15.3) + ffi (1.15.5) fiber-local (1.0.0) font-awesome-sass (4.6.2) sass (>= 3.2) - fugit (1.5.1) + fugit (1.5.2) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.4) github_changelog_generator (1.16.4) @@ -315,7 +354,7 @@ GEM octokit (~> 4.6) rainbow (>= 2.2.1) rake (>= 10.0) - globalid (0.5.2) + globalid (1.0.0) activesupport (>= 5.0) haml (5.1.2) temple (>= 0.8.0) @@ -333,9 +372,9 @@ GEM haml (>= 4.0, < 6) nokogiri (>= 1.6.0) ruby_parser (~> 3.5) - i18n (1.8.10) + i18n (1.9.1) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.34) + i18n-tasks (0.9.37) activesupport (>= 4.0.2) ast (>= 2.1.0) erubi @@ -349,68 +388,85 @@ GEM image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - io-console (0.5.9) - irb (1.3.7) - reline (>= 0.2.7) + io-console (0.5.11) + io-event (1.0.2) + io-wait (0.2.1) + irb (1.4.1) + reline (>= 0.3.0) jaro_winkler (1.5.4) jquery-rails (4.4.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.5.1) + json (2.6.1) json-schema (2.8.1) addressable (>= 2.4) json_spec (1.1.5) multi_json (~> 1.0) rspec (>= 2.0, < 4.0) - kaminari (1.2.1) + kaminari (1.2.2) activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.1) - kaminari-activerecord (= 1.2.1) - kaminari-core (= 1.2.1) - kaminari-actionview (1.2.1) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) actionview - kaminari-core (= 1.2.1) - kaminari-activerecord (1.2.1) + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) activerecord - kaminari-core (= 1.2.1) - kaminari-core (1.2.1) + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) kramdown (2.3.1) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - listen (3.7.0) + listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) localhost (1.1.9) - loofah (2.12.0) + loofah (2.13.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) mapping (1.1.1) - marcel (1.0.1) + marcel (1.0.2) memoist (0.16.2) method_source (1.0.0) middleware (0.1.0) - mime-types (3.3.1) + mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0704) + mime-types-data (3.2022.0105) mimemagic (0.3.10) nokogiri (~> 1) rake mini_magick (4.11.0) - mini_mime (1.1.1) + mini_mime (1.1.2) mini_portile2 (2.5.3) - minitest (5.14.4) + minitest (5.15.0) mono_logger (1.1.1) - msgpack (1.4.2) + msgpack (1.4.4) multi_json (1.15.0) multipart-post (2.1.1) mustache (1.1.1) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) nenv (0.3.0) + net-imap (0.2.3) + digest + net-protocol + strscan + net-pop (0.1.1) + digest + net-protocol + timeout + net-protocol (0.1.2) + io-wait + timeout + net-smtp (0.3.1) + digest + net-protocol + timeout nio4r (2.5.8) nokogiri (1.11.7) mini_portile2 (~> 2.5.0) @@ -418,7 +474,7 @@ GEM notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - octokit (4.21.0) + octokit (4.22.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) optimist (3.0.1) @@ -430,14 +486,14 @@ GEM mimemagic (~> 0.3.0) terrapin (~> 0.6.0) parallel (1.21.0) - parser (3.0.3.1) + parser (3.1.0.0) ast (~> 2.4.1) - passenger (6.0.10) + passenger (6.0.12) rack rake (>= 0.8.1) patience_diff (1.2.0) optimist (~> 3.0) - pg (1.2.3) + pg (1.3.0) process-metrics (0.2.1) console (~> 1.8) samovar (~> 2.1) @@ -466,64 +522,65 @@ GEM rack-rewrite (1.5.1) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.4.1) - actioncable (= 6.1.4.1) - actionmailbox (= 6.1.4.1) - actionmailer (= 6.1.4.1) - actionpack (= 6.1.4.1) - actiontext (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activemodel (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + rails (7.0.1) + actioncable (= 7.0.1) + actionmailbox (= 7.0.1) + actionmailer (= 7.0.1) + actionpack (= 7.0.1) + actiontext (= 7.0.1) + actionview (= 7.0.1) + activejob (= 7.0.1) + activemodel (= 7.0.1) + activerecord (= 7.0.1) + activestorage (= 7.0.1) + activesupport (= 7.0.1) bundler (>= 1.15.0) - railties (= 6.1.4.1) - sprockets-rails (>= 2.0.0) + railties (= 7.0.1) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - rails-i18n (6.0.0) + rails-i18n (7.0.1) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 7) - rails_same_site_cookie (0.1.8) + railties (>= 6.0.0, < 8) + rails_same_site_cookie (0.1.9) rack (>= 1.5) - user_agent_parser (~> 2.5) - rails_semantic_logger (4.6.1) + user_agent_parser (~> 2.6) + rails_semantic_logger (4.10.0) rack - railties (>= 3.2) - semantic_logger (~> 4.8) - railties (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + railties (>= 5.1) + semantic_logger (~> 4.9) + railties (7.0.1) + actionpack (= 7.0.1) + activesupport (= 7.0.1) method_source - rake (>= 0.13) + rake (>= 12.2) thor (~> 1.0) - rainbow (3.0.0) + zeitwerk (~> 2.5) + rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.11.0) rb-inotify (0.10.1) ffi (~> 1.0) + rbs (2.0.0) readapt (1.4.3) backport (~> 1.2) thor (~> 1.0) recaptcha (5.8.1) json - redis (4.4.0) + redis (4.5.1) redis-namespace (1.8.1) redis (>= 3.0.4) - regexp_parser (2.1.1) - reline (0.2.7) + regexp_parser (2.2.0) + reline (0.3.1) io-console (~> 0.5) rerun (0.13.1) listen (~> 3.0) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) - resque (2.1.0) + resque (2.2.0) mono_logger (~> 1.0) multi_json (~> 1.0) redis-namespace (~> 1.6) @@ -531,11 +588,11 @@ GEM vegas (~> 0.1.2) resque-job-stats (0.5.0) resque (>= 1.17, < 3) - resque-scheduler (4.4.0) + resque-scheduler (4.5.0) mono_logger (~> 1.0) redis (>= 3.3) - resque (>= 1.26) - rufus-scheduler (~> 3.2) + resque (>= 1.27) + rufus-scheduler (~> 3.2, < 3.7) reverse_markdown (2.1.1) nokogiri rexml (3.2.5) @@ -553,7 +610,7 @@ GEM rspec-expectations (>= 2.99.0.beta1) rspec-core (3.10.1) rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec-expectations (3.10.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-its (1.3.0) @@ -562,7 +619,7 @@ GEM rspec-mocks (3.10.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) - rspec-rails (5.0.2) + rspec-rails (5.1.0) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -570,42 +627,32 @@ GEM rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) rspec-support (~> 3.10) - rspec-support (3.10.2) + rspec-support (3.10.3) rspec_api_documentation (4.8.0) activesupport (>= 3.0.0) mustache (~> 1.0, >= 0.99.4) rspec (~> 3.0, >= 3.0.0) - rswag-api (2.4.0) - railties (>= 3.1, < 7.0) - rswag-specs (2.4.0) - activesupport (>= 3.1, < 7.0) - json-schema (~> 2.2) - railties (>= 3.1, < 7.0) - rswag-ui (2.4.0) - actionpack (>= 3.1, < 7.0) - railties (>= 3.1, < 7.0) - rubocop (1.23.0) + rubocop (1.25.0) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml - rubocop-ast (>= 1.12.0, < 2.0) + rubocop-ast (>= 1.15.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.13.0) + rubocop-ast (1.15.1) parser (>= 3.0.1.1) - rubocop-rspec (2.4.0) - rubocop (~> 1.0) - rubocop-ast (>= 1.1.0) + rubocop-rspec (2.8.0) + rubocop (~> 1.19) ruby-prof (1.4.3) ruby-progressbar (1.11.0) - ruby-vips (2.1.3) + ruby-vips (2.1.4) ffi (~> 1.12) ruby2_keywords (0.0.5) - ruby_parser (3.17.0) - sexp_processor (~> 4.15, >= 4.15.1) - rufus-scheduler (3.8.0) + ruby_parser (3.18.1) + sexp_processor (~> 4.16) + rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) samovar (2.1.4) console (~> 1.0) @@ -628,9 +675,9 @@ GEM sawyer (0.8.2) addressable (>= 2.3.5) faraday (> 0.8, < 2.0) - semantic_logger (4.8.2) + semantic_logger (4.10.0) concurrent-ruby (~> 1.0) - sexp_processor (4.15.3) + sexp_processor (4.16.0) shellany (0.0.1) shoulda-matchers (4.5.1) activesupport (>= 4.2.0) @@ -647,7 +694,7 @@ GEM rack (~> 2.2) rack-protection (= 2.1.0) tilt (~> 2.0) - solargraph (0.44.2) + solargraph (0.44.3) backport (~> 1.2) benchmark bundler (>= 1.17.2) @@ -662,17 +709,18 @@ GEM thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) - solargraph-rails (0.2.2.pre.4) + solargraph-rails (0.3.0) activesupport solargraph (>= 0.41.1) sprockets (4.0.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) sqlite3 (1.4.2) + strscan (3.0.1) super_diff (0.7.0) attr_extras (>= 6.2.4) diff-lcs @@ -681,31 +729,35 @@ GEM temple (0.8.2) term-ansicolor (1.7.1) tins (~> 1.0) - terminal-table (3.0.1) + terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) test-prof (1.0.7) - thor (1.1.0) + thor (1.2.1) thread_safe (0.3.6) tilt (2.0.10) timecop (0.9.4) timeliness (0.4.4) + timeout (0.2.0) timers (4.3.3) - tins (1.29.1) + tins (1.31.0) sync + traces (0.4.1) turnip (4.3.0) cucumber-gherkin (~> 14.0) rspec (>= 3.0, < 4.0) + typeprof (0.21.2) + rbs (>= 1.8.1) typhoeus (1.4.0) ethon (>= 0.9.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) - tzinfo-data (1.2021.1) + tzinfo-data (1.2021.5) tzinfo (>= 1.0.0) unicode-display_width (2.1.0) uniform_notifier (1.14.2) - user_agent_parser (2.7.0) + user_agent_parser (2.8.0) uuidtools (2.1.5) validates_timeliness (6.0.0.alpha1) timeliness (>= 0.3.10, < 1) @@ -723,7 +775,7 @@ GEM websocket-extensions (0.1.5) yard (0.9.27) webrick (~> 1.7.0) - zeitwerk (2.4.2) + zeitwerk (2.5.3) zonebie (0.6.1) PLATFORMS @@ -731,17 +783,17 @@ PLATFORMS DEPENDENCIES aasm (> 5) - actionmailer (~> 6.1.4) - actionview (~> 6.1.4) + actionmailer (~> 7.0.1) + actionview (~> 7.0.1) active_storage_validations - activejob (~> 6.1.4) - activestorage (~> 6.1.4) - activesupport (~> 6.1.4) + activejob (~> 7.0.1) + activestorage (~> 7.0.1) + activesupport (~> 7.0.1) acts_as_paranoid addressable amazing_print annotate - arel_extensions + arel_extensions (>= 2.1.0) async! async-http! bcrypt (~> 3.1.9) @@ -751,7 +803,7 @@ DEPENDENCIES cancancan (> 3) coderay comfortable_mexican_sofa (~> 2.0.0) - composite_primary_keys (~> 13) + composite_primary_keys (~> 14) concurrent-ruby (~> 1) concurrent-ruby-edge config @@ -761,7 +813,7 @@ DEPENDENCIES debug (>= 1.0.0) deep_sort descriptive-statistics - devise (~> 4.7.0) + devise (~> 4.8.1) devise-i18n dotiw dry-monads @@ -796,14 +848,13 @@ DEPENDENCIES rack-cors (~> 1.1.1) rack-mini-profiler (>= 2.0.2) rack-rewrite (~> 1.5.1) - rails (~> 6.1.4) + rails (~> 7.0.1) rails_same_site_cookie - rails_semantic_logger + rails_semantic_logger (>= 4.10.0) readapt recaptcha redis (~> 4.1) rerun - responders (~> 3.0.1) resque resque-job-stats resque-scheduler @@ -815,9 +866,9 @@ DEPENDENCIES rspec-mocks rspec-rails rspec_api_documentation (~> 4.8.0) - rswag-api - rswag-specs - rswag-ui + rswag-api! + rswag-specs! + rswag-ui! rubocop rubocop-rspec ruby-prof (>= 0.17.0) @@ -832,6 +883,7 @@ DEPENDENCIES test-prof timecop turnip + typeprof typhoeus tzinfo tzinfo-data @@ -842,4 +894,4 @@ DEPENDENCIES zonebie BUNDLED WITH - 2.2.15 + 2.3.4 diff --git a/app/controllers/audio_recordings/downloader_controller.rb b/app/controllers/audio_recordings/downloader_controller.rb index de0d7c6d..66fd3bb0 100644 --- a/app/controllers/audio_recordings/downloader_controller.rb +++ b/app/controllers/audio_recordings/downloader_controller.rb @@ -1,16 +1,18 @@ # frozen_string_literal: true module AudioRecordings + # Controller for creating downloader scripts class DownloaderController < ApplicationController include Api::ControllerHelper + VIEW_NAME = 'audio_recordings/downloader/download_audio_files_ps1' SCRIPT_NAME = 'download_audio_files.ps1' # GET|POST /audio_recordings/downloader # Returns a script that can be used to download media segments or files. # Accepts an audio_recordings filter object via POST body to filter results. def index - do_authorize_class + do_authorize_class(:index, :downloader) # we're not actually doing a query, discard it query, opts = Settings.api_response.response_advanced( @@ -27,16 +29,12 @@ def index @model = OpenStruct.new({ app_version: Settings.version_string, user_name: current_user&.user_name || '', - filter: filter, + filter:, workbench_url: root_url.chop }) - script = render_to_string SCRIPT_NAME, layout: false + script = render_to_string VIEW_NAME, layout: false send_data script, type: 'text/plain', filename: SCRIPT_NAME, disposition: 'attachment' end - - def resource_class - :downloader - end end end diff --git a/app/models/analysis_job.rb b/app/models/analysis_job.rb index 0a694c3c..ea5122b0 100644 --- a/app/models/analysis_job.rb +++ b/app/models/analysis_job.rb @@ -75,14 +75,14 @@ class AnalysisJob < ApplicationRecord # overall_count is the number of audio_recordings/resque jobs. These should be equal. validates :overall_count, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :overall_duration_seconds, presence: true, - numericality: { only_integer: false, greater_than_or_equal_to: 0 } + numericality: { only_integer: false, greater_than_or_equal_to: 0 } validates :overall_status_modified_at, :overall_progress_modified_at, - presence: true, timeliness: { on_or_before: -> { Time.zone.now }, type: :datetime } - validates :started_at, allow_blank: true, allow_nil: true, timeliness: { on_or_before: lambda { - Time.zone.now - }, type: :datetime } + presence: true, timeliness: { on_or_before: -> { Time.zone.now }, type: :datetime } + validates :started_at, allow_blank: true, allow_nil: true, timeliness: { + on_or_before: -> { Time.zone.now }, type: :datetime + } validates :overall_data_length_bytes, presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + numericality: { only_integer: true, greater_than_or_equal_to: 0 } renders_markdown_for :description @@ -293,7 +293,7 @@ def check_progress # retry just the failures event :retry do transitions from: :completed, to: :processing, - guard: :are_any_job_items_failed?, after: [:retry_job, :send_retry_email] + guard: :are_any_job_items_failed?, after: [:retry_job, :send_retry_email] end after_all_transitions :update_status_timestamp @@ -303,7 +303,7 @@ def check_progress AVAILABLE_JOB_STATUS_SYMBOLS = aasm.states.map(&:name) AVAILABLE_JOB_STATUS = AVAILABLE_JOB_STATUS_SYMBOLS.map(&:to_s) - AVAILABLE_JOB_STATUS_DISPLAY = aasm.states.map { |x| [x.name, x.display_name] }.to_h + AVAILABLE_JOB_STATUS_DISPLAY = aasm.states.to_h { |x| [x.name, x.display_name] } # hook active record callbacks into state machine before_validation(on: :create) do @@ -456,7 +456,7 @@ def update_job_progress self.overall_progress = statuses self.overall_progress_modified_at = Time.zone.now - { overall_progress: overall_progress, overall_progress_modified_at: overall_progress_modified_at } + { overall_progress:, overall_progress_modified_at: } end # diff --git a/app/models/audio_recording.rb b/app/models/audio_recording.rb index 9e1c6dc7..6b7615fa 100644 --- a/app/models/audio_recording.rb +++ b/app/models/audio_recording.rb @@ -72,9 +72,9 @@ class AudioRecording < ApplicationRecord belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_audio_recordings belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_audio_recordings, - optional: true + optional: true belongs_to :deleter, class_name: 'User', foreign_key: :deleter_id, inverse_of: :deleted_audio_recordings, - optional: true + optional: true belongs_to :uploader, class_name: 'User', foreign_key: :uploader_id, inverse_of: :uploaded_audio_recordings accepts_nested_attributes_for :site @@ -111,7 +111,7 @@ class AudioRecording < ApplicationRecord validates :uuid, presence: true, length: { is: 36 }, uniqueness: { case_sensitive: false } validates :recorded_date, presence: true, timeliness: { on_or_before: -> { Time.zone.now }, type: :datetime } validates :duration_seconds, presence: true, - numericality: { greater_than_or_equal_to: Settings.audio_recording_min_duration_sec } + numericality: { greater_than_or_equal_to: Settings.audio_recording_min_duration_sec } validates :sample_rate_hertz, presence: true, numericality: { only_integer: true, greater_than: 0 } # the channels field encodes our special version of a bit flag. 0 (no bits flipped) represents @@ -123,12 +123,12 @@ class AudioRecording < ApplicationRecord # file hash validations # on create, ensure present, case insensitive unique, starts with 'SHA256::', and exactly 72 chars validates :file_hash, presence: true, uniqueness: { case_sensitive: false }, length: { is: 72 }, - format: { with: /\ASHA256#{HASH_TOKEN}.{64}\z/, message: "must start with \"SHA256#{HASH_TOKEN}\" with 64 char hash" }, - on: :create + format: { with: /\ASHA256#{HASH_TOKEN}.{64}\z/, message: "must start with \"SHA256#{HASH_TOKEN}\" with 64 char hash" }, + on: :create # on update would usually be the same, but for the audio check this needs to ignore validates :file_hash, presence: true, uniqueness: { case_sensitive: false }, length: { is: 72 }, - format: { with: /\ASHA256#{HASH_TOKEN}.{64}\z/, message: "must start with \"SHA256#{HASH_TOKEN}\" with 64 char hash" }, - on: :update, unless: :missing_hash_value? + format: { with: /\ASHA256#{HASH_TOKEN}.{64}\z/, message: "must start with \"SHA256#{HASH_TOKEN}\" with 64 char hash" }, + on: :update, unless: :missing_hash_value? after_initialize :set_uuid @@ -195,7 +195,7 @@ def original_file_paths source_existing_paths = [] unless original_format.blank? modify_parameters = { - uuid: uuid, + uuid:, datetime_with_offset: recorded_date, original_format: original_format_calculated } @@ -210,7 +210,7 @@ def original_file_paths # gets the 'ideal' file name (not path) for an audio recording that is stored on disk def canonical_filename modify_parameters = { - uuid: uuid, + uuid:, datetime_with_offset: recorded_date, original_format: original_format_calculated } diff --git a/app/models/current.rb b/app/models/current.rb index 9b1bfe14..62ebfe08 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -4,10 +4,10 @@ # https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html#method-c-attribute class Current < ActiveSupport::CurrentAttributes # @!attribute [rw] user - # @return [User] + # @return [User] The current user for the request attribute :user # @!attribute [rw] ability - # @return [Ability] + # @return [Ability] The current ability for the request attribute :ability # @!parse diff --git a/app/modules/filter/build.rb b/app/modules/filter/build.rb index e6e3a827..2274d541 100644 --- a/app/modules/filter/build.rb +++ b/app/modules/filter/build.rb @@ -63,14 +63,14 @@ def initialize(table, filter_settings) def projections(hash) if hash.blank? || hash.size != 1 raise CustomErrors::FilterArgumentError.new("Projections hash must have exactly 1 entry, got #{hash.size}.", - { hash: hash }) + { hash: }) end result = [] hash.each do |key, value| unless [:include, :exclude].include?(key) raise CustomErrors::FilterArgumentError.new("Must be 'include' or 'exclude' at top level, got #{key}", - { hash: hash }) + { hash: }) end result = projection(key, value) @@ -80,9 +80,15 @@ def projections(hash) # Build projection to include or exclude. # @param [Symbol] key - # @param [Hash] value + # @param [Array] value # @return [Array] projections def projection(key, value) + unless value.is_a?(Array) + raise CustomErrors::FilterArgumentError.new( + "Projection field list must be an array but instead got #{value.class}", { key.to_s => value } + ) + end + if !value.blank? && value.uniq.length != value.length raise CustomErrors::FilterArgumentError.new('Must not contain duplicate fields.', { key.to_s => value }) end @@ -125,7 +131,7 @@ def combiner_two(combiner, condition1, condition2) when :or compose_or(condition1, condition2) else - raise CustomErrors::FilterArgumentError, "Unrecognised filter combiner #{combiner}." + raise CustomErrors::FilterArgumentError, "Unrecognized filter combiner #{combiner}." end end @@ -327,10 +333,10 @@ def parse_filter(primary, secondary = nil, extra = nil) end else - raise CustomErrors::FilterArgumentError, "Unrecognised combiner or field name: #{primary}." + raise CustomErrors::FilterArgumentError, "Unrecognized combiner or field name: #{primary}." end else - raise CustomErrors::FilterArgumentError, "Unrecognised filter component: #{primary}." + raise CustomErrors::FilterArgumentError, "Unrecognized filter component: #{primary}." end end @@ -394,7 +400,7 @@ def condition(filter_name, table, column_name, valid_fields, filter_value) # unknown else - raise CustomErrors::FilterArgumentError, "Unrecognised filter #{filter_name}." + raise CustomErrors::FilterArgumentError, "Unrecognized filter #{filter_name}." end end @@ -456,7 +462,7 @@ def condition_node(filter_name, node, filter_value) # unknown else - raise CustomErrors::FilterArgumentError, "Unrecognised filter #{filter_name}." + raise CustomErrors::FilterArgumentError, "Unrecognized filter #{filter_name}." end end @@ -512,7 +518,7 @@ def parse_table_field(table, field) table_name: table.name, field_name: field, arel_table: table, - model: model, + model:, filter_settings: @filter_settings } end @@ -552,8 +558,8 @@ def parse_other_table_field(table, field) { table_name: parsed_table, field_name: parsed_field, - arel_table: arel_table, - model: model, + arel_table:, + model:, filter_settings: model_filter_settings } end @@ -614,8 +620,8 @@ def build_associations(valid_associations, table) if available associations.push( { - join: join, - on: on + join:, + on: } ) end diff --git a/app/modules/filter/query.rb b/app/modules/filter/query.rb index 4d7a053b..8f24dfcd 100644 --- a/app/modules/filter/query.rb +++ b/app/modules/filter/query.rb @@ -57,6 +57,8 @@ def initialize(parameters, query, model, filter_settings) @parameters = CleanParams.perform(parameters) validate_hash(@parameters) + @parameters = decode_payload(@parameters) + @filter = @parameters.include?(:filter) && !@parameters[:filter].blank? ? @parameters[:filter] : {} @projection = parse_projection(@parameters) @@ -217,6 +219,39 @@ def has_filter_params? private + def decode_payload(parameters) + return parameters unless parameters.include?(:filter_encoded) + + parameters.extract!(:filter_encoded) => {filter_encoded: value} + + return parameters if value.blank? + + json = begin + Base64.urlsafe_decode64(value) + rescue StandardError + raise CustomErrors::FilterArgumentError, 'filter_encoded was not a valid RFC 4648 base64url string' + end + + hash = begin + JSON.parse(json) + rescue StandardError => e + # JSON parser returns line where the error occurred in the C source code... + # so basically useless. Remove it. + message = e.message.gsub(/\d+: /, '') + error = "filter_encoded was not a valid JSON payload: #{message}." \ + "Check the filter is valid JSON and it was not truncated. We received value of size #{value.length}." + raise CustomErrors::FilterArgumentError, error + end + + # parameters has already been cleaned, but hash has just been deserialized! + # so clean and normalize names here too + hash = CleanParams.perform(hash) + + parameters.deep_merge!(hash) + + parameters + end + # Add qsp spec to filter # @param [Hash] filter # @param [Hash] additional diff --git a/app/modules/time_zone_attribute.rb b/app/modules/time_zone_attribute.rb index dc0f0437..beaf7f38 100644 --- a/app/modules/time_zone_attribute.rb +++ b/app/modules/time_zone_attribute.rb @@ -26,6 +26,7 @@ def tzinfo_tz=(value) else value end + # store the actual IANA identifier write_attribute(:tzinfo_tz, normalized) write_attribute(:rails_tz, TimeZoneHelper.tz_identifier_to_ruby(normalized)) @@ -63,6 +64,6 @@ def check_tz suggestions = TimeZoneHelper.tzinfo_friendly_did_you_mean(tzinfo_identifier)[0..2] msg1 = "is not a recognized timezone ('#{tzinfo_identifier}'" msg2 = suggestions.any? ? " - did you mean '#{suggestions.join("', '")}'?)" : ')' - errors[:tzinfo_tz] << msg1 + msg2 + errors.add(:tzinfo_tz, msg1 + msg2) end end diff --git a/app/views/audio_recordings/downloader/download_audio_files.ps1.erb b/app/views/audio_recordings/downloader/download_audio_files_ps1.erb similarity index 100% rename from app/views/audio_recordings/downloader/download_audio_files.ps1.erb rename to app/views/audio_recordings/downloader/download_audio_files_ps1.erb diff --git a/baw-server.code-workspace b/baw-server.code-workspace index fba7e679..a33b524d 100644 --- a/baw-server.code-workspace +++ b/baw-server.code-workspace @@ -23,6 +23,7 @@ "bootsnap", "Bootsnap", "bowra", + "cancancan", "chown", "cleanpath", "completionist", @@ -149,6 +150,7 @@ "uploader", "uploaders", "upsert", + "urlsafe", "uuid", "wavpack", "webdav", @@ -269,7 +271,8 @@ "pavlitsky.yard", "ms-azuretools.vscode-docker", "github.copilot", - "KoichiSasada.vscode-rdbg" + "koichisasada.vscode-rdbg", + "mame.ruby-typeprof" ] } } diff --git a/config/application.rb b/config/application.rb index 8c81dade..1e35c332 100644 --- a/config/application.rb +++ b/config/application.rb @@ -77,7 +77,7 @@ class Application < Rails::Application # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - config.load_defaults '6.0' + config.load_defaults '7.0' # TODO: fix, dangerous! # https://stackoverflow.com/questions/53878453/upgraded-rails-to-6-getting-blocked-host-error diff --git a/config/environments/test.rb b/config/environments/test.rb index efef6ef9..e8633b7f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/lib/gems/baw-workers/lib/baw_workers/active_job/extensions.rb b/lib/gems/baw-workers/lib/baw_workers/active_job/extensions.rb index c5de1206..3d1ff9f0 100644 --- a/lib/gems/baw-workers/lib/baw_workers/active_job/extensions.rb +++ b/lib/gems/baw-workers/lib/baw_workers/active_job/extensions.rb @@ -20,12 +20,6 @@ module Extensions # Class method extensions for ActiveJob module ClassMethods include ::Dry::Monads[:result] - - # TODO: when Rails 7 is released, these methods may be able to change - # to be simpler wrappers because perform_later now accepts a block - # that is always invoked. - # See: https://github.com/rails/rails/blob/main/activejob/lib/active_job/enqueuing.rb - # (see #perform_later) # The same as #perform_later except will raise if job was not successfully enqueued. # It also supports a block callback on failure to enqueue - which is missing in @@ -33,8 +27,9 @@ module ClassMethods # @raise [StandardError] when the job fails to enqueue # @return [void] def perform_later!(...) + # mirrors the definition of + # https://github.com/rails/rails/blob/main/activejob/lib/active_job/enqueuing.rb job = job_or_instantiate(...) - result = job.enqueue yield job if block_given? @@ -44,13 +39,6 @@ def perform_later!(...) result end - def perform_later(...) - logger.warn 'Perform later is tricky to use; it has a variant return type and will not surface errors as we expect' - raise 'perform_later does not support blocks' if ::Rails::VERSION::MAJOR < 7 && block_given? - - super(...) - end - # (see #perform_later) # The same as #perform_later except always returns the job wrapped in # a ::Dry::Monad::Result. @@ -78,5 +66,3 @@ def try_perform_later(...) end end end - -raise 'Fix the rails patches in BawWorkers::ActiveJob::Extensions' if ::Rails::VERSION::MAJOR >= 7 diff --git a/lib/gems/baw-workers/lib/baw_workers/api_communicator.rb b/lib/gems/baw-workers/lib/baw_workers/api_communicator.rb index 78dda1be..96235618 100644 --- a/lib/gems/baw-workers/lib/baw_workers/api_communicator.rb +++ b/lib/gems/baw-workers/lib/baw_workers/api_communicator.rb @@ -80,7 +80,7 @@ def send_request(description, method, host, port, endpoint, security_info = { au when :patch request = Net::HTTP::Patch.new(endpoint) else - raise ArgumentError, "Unrecognised HTTP method #{method}." + raise ArgumentError, "Unrecognized HTTP method #{method}." end request['Content-Type'] = 'application/json' @@ -151,7 +151,7 @@ def send_request(description, method, host, port, endpoint, security_info = { au # @return [Hash] def request_login login_response = send_request('Login request', :post, host, port, endpoint_login, nil, - { email: user, password: password }) + { email: user, password: }) # get cookies # from http://stackoverflow.com/a/9320190/31567 @@ -175,7 +175,7 @@ def request_login { auth_token: json_resp['data']['auth_token'], - cookies: cookies + cookies: } else @logger.error(@class_name) do @@ -184,7 +184,7 @@ def request_login { auth_token: nil, - cookies: cookies + cookies: } end end @@ -193,7 +193,7 @@ def request_login def update_audio_recording_details(description, file_to_process, audio_recording_id, update_hash, security_info) endpoint = endpoint_audio_recording.gsub(':id', audio_recording_id.to_s) response = send_request("Update audio recording metadata - #{description}", :put, host, port, endpoint, - security_info, update_hash) + security_info, update_hash) msg = "Code #{response.code}, Id: #{audio_recording_id}, Hash: '#{update_hash}', File: '#{file_to_process}'" if response.code.to_i == 200 || response.code.to_i == 204 @@ -272,12 +272,12 @@ def create_new_audio_recording(file_to_process, project_id, site_id, audio_info_ @logger.info(@class_name) do "[HTTP] Created new audio recording. Id: #{response_json.dig('data', 'id')}, #{msg}" end - { response: response, response_json: response_json } + { response:, response_json: } else @logger.error(@class_name) do "[HTTP] Problem creating new audio recording. #{msg}" end - { response: response, response_json: nil } + { response:, response_json: nil } end end @@ -291,7 +291,7 @@ def create_new_audio_recording(file_to_process, project_id, site_id, audio_info_ def update_audio_recording_status(description, file_to_process, audio_recording_id, update_hash, security_info) endpoint = endpoint_audio_recording_update_status.gsub(':id', audio_recording_id.to_s) response = send_request("Update audio recording status - #{description}", :put, host, port, endpoint, - security_info, update_hash) + security_info, update_hash) msg = "'#{description}'. Code #{response.code}, File: '#{file_to_process}', Id: #{audio_recording_id}, Hash: '#{update_hash}'" if response.code.to_i == 200 || response.code.to_i == 204 @logger.info(@class_name) do @@ -332,12 +332,12 @@ def get_analysis_jobs_item_status(analysis_job_id, audio_recording_id, security_ @logger.info(@class_name) do "[HTTP] Retrieved status for analysis job item: #{status}, #{msg}" end - { response: response, failed: false, response_json: response_json, status: status } + { response:, failed: false, response_json:, status: } else @logger.error(@class_name) do "[HTTP] Problem retrieving status for analysis job item: #{msg}" end - { response: response, failed: true, response_json: nil, status: nil } + { response:, failed: true, response_json: nil, status: nil } end end @@ -363,7 +363,7 @@ def update_analysis_jobs_item_status(analysis_job_id, audio_recording_id, status endpoint, security_info, { - status: status + status: } ) @@ -376,16 +376,16 @@ def update_analysis_jobs_item_status(analysis_job_id, audio_recording_id, status "[HTTP] Audio recording status updated for #{msg}" end { - response: response, + response:, failed: false, - response_json: response_json, + response_json:, status: response_json.nil? ? nil : response_json['data']['status'] } else @logger.error(@class_name) do "[HTTP] Problem updating audio recording status for #{msg}" end - { response: response, failed: true, response_json: nil, status: nil } + { response:, failed: true, response_json: nil, status: nil } end end end diff --git a/lib/gems/baw-workers/lib/baw_workers/redis_communicator.rb b/lib/gems/baw-workers/lib/baw_workers/redis_communicator.rb index 9b5b6b4b..034bd066 100644 --- a/lib/gems/baw-workers/lib/baw_workers/redis_communicator.rb +++ b/lib/gems/baw-workers/lib/baw_workers/redis_communicator.rb @@ -68,7 +68,7 @@ def delete_all(key) def exists(key, opts = {}) key = add_namespace(key) unless opts[:no_namespace] - @redis.exists?(key) + boolify(@redis.exists?(key)) end # @param [String] key @@ -77,7 +77,7 @@ def exists(key, opts = {}) def exists?(key, opts = {}) key = add_namespace(key) unless opts[:no_namespace] - @redis.exists?(key) + boolify(@redis.exists?(key)) end # @param [String] key @@ -117,7 +117,7 @@ def set_file(key, path) key = safe_file_name(key) key = add_namespace(key) - result = logger.measure_debug('storing large binary key', payload: { key: key, size: path.size }) { + result = logger.measure_debug('storing large binary key', payload: { key:, size: path.size }) { @redis.set( key.force_encoding(BINARY_ENCODING), File.binread(path), @@ -135,7 +135,7 @@ def exists_file?(key) key = safe_file_name(key) key = add_namespace(key) - redis.exists?(key) + boolify(redis.exists?(key)) end def delete_file(key) @@ -167,7 +167,7 @@ def get_file(key, dest) end begin - result = logger.measure_debug('fetching large binary key', payload: { key: key, dest: dest.to_s }) { + result = logger.measure_debug('fetching large binary key', payload: { key:, dest: dest.to_s }) { # i say binary response, but it's just a string binary_response = @redis.get(key.force_encoding(BINARY_ENCODING)) return nil if binary_response.nil? @@ -240,7 +240,14 @@ def safe_file_name(key) end def boolify(value) - value.is_a?(String) && value == 'OK' + case value + when String + value == 'OK' + when Integer + value.positive? + else + value + end end end end diff --git a/lib/gems/baw-workers/lib/patches/pause_dequeue_for_tests.rb b/lib/gems/baw-workers/lib/patches/pause_dequeue_for_tests.rb index b066e31a..d0414f4c 100644 --- a/lib/gems/baw-workers/lib/patches/pause_dequeue_for_tests.rb +++ b/lib/gems/baw-workers/lib/patches/pause_dequeue_for_tests.rb @@ -67,7 +67,7 @@ def self.should_dequeue_job? case should_dequeue when TEST_PERFORM_PAUSED # Do not dequeue. Wait for next round. This is the pause. - logger.info('did not execute job because work is paused', test_perform: should_dequeue) + logger.debug('did not execute job because work is paused', test_perform: should_dequeue) false when TEST_PERFORM_UNLOCKED, nil # Act like this plugin is not activated. Do nothing. diff --git a/lib/patches/rails_semantic_logger/active_record_log_subscriber.rb b/lib/patches/rails_semantic_logger/active_record_log_subscriber.rb new file mode 100644 index 00000000..a1619332 --- /dev/null +++ b/lib/patches/rails_semantic_logger/active_record_log_subscriber.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RailsSemanticLogger + module ActiveRecord + class LogSubscriber + # def self.prepended(target) + # target.instance_eval do + alias bind_values bind_values_v6_1 + alias render_bind render_bind_v6_1 + alias type_casted_binds type_casted_binds_v5_1_5 + # end + # end + end + end +end + +if Gem.loaded_specs['rails_semantic_logger'].version > Gem::Version.new('4.10.0') + raise "remove #{__FILE__} patch if https://github.com/reidmorrison/rails_semantic_logger/issues/156 resolved" +end diff --git a/lib/tasks/fix_audio_recording_notes.rake b/lib/tasks/fix_audio_recording_notes.rake index d6bc95aa..d18d5e62 100644 --- a/lib/tasks/fix_audio_recording_notes.rake +++ b/lib/tasks/fix_audio_recording_notes.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'csv' require 'json' @@ -5,9 +7,8 @@ require 'json' # bin/rake baw:audio_recordings:repair_notes RAILS_ENV=staging namespace :baw do namespace :audio_recordings do - desc 'Fix format of notes field for audio recordings.' - task :repair_notes => :environment do |t, args| + task repair_notes: :environment do |_t, _args| puts '' puts "Checking #{AudioRecording.where('notes IS NOT NULL').count} audio recording notes..." puts '' @@ -18,13 +19,13 @@ namespace :baw do # ******** AudioRecording.where('notes IS NOT NULL').order(id: :asc).find_each do |ar| # .where('id > 240000') ar_notes = AudioRecording.connection.select_all(AudioRecording.where(id: ar.id).select(:notes).to_sql).first['notes'] - + # don't try to fix things that are already json if valid_json?(ar_notes) print '-' next end - + # fix a previous mistake we made, RE: storing an escaped JSON string is_escaped, parsed_hash = escaped_json?(ar_notes) if is_escaped @@ -32,8 +33,8 @@ namespace :baw do print '*' next end - - note_lines = ar_notes.split(/\r\n|\n\r|\r|\n/).reject { |item| item.blank? } + + note_lines = ar_notes.split(/\r\n|\n\r|\r|\n/).reject(&:blank?) total_note_lines = note_lines.size processed_note_lines = 0 @@ -50,7 +51,7 @@ namespace :baw do # check that all lines are INI-style if processed_note_lines < total_note_lines - fail ArgumentError, "Didn't process all lines for audio recording id #{ar.id}." + raise ArgumentError, "Didn't process all lines for audio recording id #{ar.id}." end # processing @@ -60,48 +61,46 @@ namespace :baw do fix_overlap(result_hash) -=begin - # check that all lines are recognised - single_value_keys = [ - 'dep_entity_id', 'dep_entity_name', 'dep_entity_createdby', 'dep_entity_createdby_name', - 'dep_entity_type', 'dep_entity_depid', 'dep_entity_createddate', 'dep_id', 'dep_hardwareid', - 'dep_name', 'dep_datestarted', 'dep_desc', 'dep_istest', 'dep_createdby', 'dep_createdby_name', - 'dep_createdtime', 'dep_isactive', 'dep_timeout', 'dep_issensitive', 'hardware_id', - 'hardware_uniqueid', 'hardware_friendly', 'hardware_manual', 'hardware_createdby', - 'hardware_createdby_name', 'hardware_createdtime', 'site_id', 'site_name', 'site_createdby', - 'site_createdby_name', 'site_type', 'site_geo', 'site_geo_lat', 'site_geo_long', 'site_geobinary', - 'site_geobinary_lat', 'site_geobinary_long', 'site_createddate', 'dep_approx_long', 'dep_approx_lat', - 'dep_locationapprox', 'dep_locationapprox_lat', 'dep_locationapprox_long', 'dep_locationapproxbinary', - 'dep_locationapproxbinary_lat', 'dep_locationapproxbinary_long', 'dep_long', 'dep_lat', 'dep_location', - 'dep_location_lat', 'dep_location_long', 'dep_locationbinary', 'dep_locationbinary_lat', - 'dep_locationbinary_long', 'hardware_lastcontacted', 'dep_entity_geobinary', 'dep_entity_geobinary_lat', - 'dep_entity_geobinary_long', 'dep_datended', 'UploadStartUTC', - - 'dep_entity_notes', 'dep_notes', 'site_notes', - - 'duration_adjustment_for_overlap' - ] - require_processing_keys = [] - recognised_keys = single_value_keys + require_processing_keys - comparison = result_hash.keys - recognised_keys - unless comparison.empty? - fail ArgumentError, "Unrecognised keys for id #{ar.id}: #{comparison.join(', ')}." - end - - if result_hash.keys.to_set.intersect?(require_processing_keys.to_set) - - puts '' - puts "Notes for #{ar.id}:" - - result_hash.slice(*require_processing_keys).each do |key, value| - puts '' - print key, ':', value - end - puts '' - else - #print '.' - end -=end + # # check that all lines are recognised + # single_value_keys = [ + # 'dep_entity_id', 'dep_entity_name', 'dep_entity_createdby', 'dep_entity_createdby_name', + # 'dep_entity_type', 'dep_entity_depid', 'dep_entity_createddate', 'dep_id', 'dep_hardwareid', + # 'dep_name', 'dep_datestarted', 'dep_desc', 'dep_istest', 'dep_createdby', 'dep_createdby_name', + # 'dep_createdtime', 'dep_isactive', 'dep_timeout', 'dep_issensitive', 'hardware_id', + # 'hardware_uniqueid', 'hardware_friendly', 'hardware_manual', 'hardware_createdby', + # 'hardware_createdby_name', 'hardware_createdtime', 'site_id', 'site_name', 'site_createdby', + # 'site_createdby_name', 'site_type', 'site_geo', 'site_geo_lat', 'site_geo_long', 'site_geobinary', + # 'site_geobinary_lat', 'site_geobinary_long', 'site_createddate', 'dep_approx_long', 'dep_approx_lat', + # 'dep_locationapprox', 'dep_locationapprox_lat', 'dep_locationapprox_long', 'dep_locationapproxbinary', + # 'dep_locationapproxbinary_lat', 'dep_locationapproxbinary_long', 'dep_long', 'dep_lat', 'dep_location', + # 'dep_location_lat', 'dep_location_long', 'dep_locationbinary', 'dep_locationbinary_lat', + # 'dep_locationbinary_long', 'hardware_lastcontacted', 'dep_entity_geobinary', 'dep_entity_geobinary_lat', + # 'dep_entity_geobinary_long', 'dep_datended', 'UploadStartUTC', + # + # 'dep_entity_notes', 'dep_notes', 'site_notes', + # + # 'duration_adjustment_for_overlap' + # ] + # require_processing_keys = [] + # recognised_keys = single_value_keys + require_processing_keys + # comparison = result_hash.keys - recognised_keys + # unless comparison.empty? + # fail ArgumentError, "Unrecognized keys for id #{ar.id}: #{comparison.join(', ')}." + # end + # + # if result_hash.keys.to_set.intersect?(require_processing_keys.to_set) + # + # puts '' + # puts "Notes for #{ar.id}:" + # + # result_hash.slice(*require_processing_keys).each do |key, value| + # puts '' + # print key, ':', value + # end + # puts '' + # else + # #print '.' + # end ar.update_columns(notes: result_hash, updated_at: Time.zone.now) #ar.notes(result_hash) #ar.save!(validate: false) @@ -110,7 +109,6 @@ namespace :baw do puts '' puts '...done.' - end end end @@ -118,7 +116,7 @@ end def ensure_new_key(hash, key, value) append_num = 0 loop do - current_key = append_num > 0 ? "#{key}_#{append_num}" : key + current_key = append_num.positive? ? "#{key}_#{append_num}" : key if hash.include?(current_key) append_num += 1 else @@ -130,15 +128,14 @@ end def fix_double_quotes(hash, key) if hash.include?(key) && - !hash[key].blank? && - hash[key].start_with?('\\"') && - hash[key].end_with?('\\"') + !hash[key].blank? && + hash[key].start_with?('\\"') && + hash[key].end_with?('\\"') hash[key] = hash[key][2..-3] end end def fix_overlap(hash) - key = 'duration_adjustment_for_overlap' infos = [] @@ -148,77 +145,68 @@ def fix_overlap(hash) # parse each value and add object to overlap array values.each do |overlap_string| - # Change made 2014-12-01T00:10:37Z: overlap of 10.004800081253052 seconds with audio_recording with uuid 8284b364-b1ee-491a-a2bf-8b489e1d94b8. regex = /^Change made (.+): overlap of (.+) seconds with audio_recording with uuid (.+)\.$/ overlap_string.scan(regex) do |changed_at, overlap_amount, other_uuid| infos.push({ - changed_at: changed_at, - overlap_amount: overlap_amount.to_f, - old_duration: nil, - new_duration: nil, - other_uuid: other_uuid - }) + changed_at:, + overlap_amount: overlap_amount.to_f, + old_duration: nil, + new_duration: nil, + other_uuid: + }) end # Change made 2015-07-10T06:34:38Z: overlap of 1.003 seconds (duration: old: 23008.003, new: 23007.0) with audio_recording with uuid 12d8eb2e-c793-499e-baf1-14ff163f90c0. regex = /^Change made (.+): overlap of (.+) seconds \(duration: old: (.+), new: (.+)\) with audio_recording with uuid (.+)\.$/ overlap_string.scan(regex) do |changed_at, overlap_amount, old_duration, new_duration, other_uuid| infos.push({ - changed_at: changed_at, - overlap_amount: overlap_amount.to_f, - old_duration: old_duration.to_f, - new_duration: new_duration.to_f, - other_uuid: other_uuid - }) + changed_at:, + overlap_amount: overlap_amount.to_f, + old_duration: old_duration.to_f, + new_duration: new_duration.to_f, + other_uuid: + }) end # Change made 2015-06-11T06:03:28Z: overlap of 0.003 seconds with audio_recording with uuid . regex = /^Change made (.+): overlap of (.+) seconds with audio_recording with uuid \.$/ - overlap_string.scan(regex) do |changed_at, overlap_amount | + overlap_string.scan(regex) do |changed_at, overlap_amount| infos.push({ - changed_at: changed_at, - overlap_amount: overlap_amount.to_f, - old_duration: nil, - new_duration: nil, - other_uuid: nil - }) + changed_at:, + overlap_amount: overlap_amount.to_f, + old_duration: nil, + new_duration: nil, + other_uuid: nil + }) end - end -=begin - if keys.size > 0 && infos.size != keys.size - puts '' - puts keys - puts values - puts infos - puts '' - end -=end + # if keys.size > 0 && infos.size != keys.size + # puts '' + # puts keys + # puts values + # puts infos + # puts '' + # end # remove overlap_strings from hash keys.each { |k| hash.delete(k) } # save infos back to hash hash['duration_adjustment_for_overlap'] = infos - end def valid_json?(json) - begin - JSON.parse(json) - return true - rescue JSON::ParserError => e - return false - end + JSON.parse(json) + true +rescue JSON::ParserError => e + false end def escaped_json?(json) - begin - obj = JSON.parse(ActiveSupport::JSON.decode(json)) - return true, obj - rescue JSON::ParserError => e - return false, nil - end + obj = JSON.parse(ActiveSupport::JSON.decode(json)) + [true, obj] +rescue JSON::ParserError => e + [false, nil] end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index dedadf11..55d68259 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -195,11 +195,15 @@ def invoke(test_value, expected = :unprocessable_entity) controller(ApplicationController) do skip_authorization_check + attr_accessor :ability, :user + def index_ability + @ability = Current.ability render plain: Current.ability&.class&.name end def index_user + @user = Current.user render(plain: Current.user&.user_name) end end @@ -221,33 +225,34 @@ def index_user it 'sets Current.user when a user session is supplied' do request.env['HTTP_AUTHORIZATION'] = reader_token response = get :index_user - expect(Current.user).to eq reader_user + + expect(controller.user).to eq reader_user expect(response.body).to eq(reader_user.user_name) end it 'sets Current.user to nil when there is no user session' do response = get :index_user - expect(Current.user).to be_nil + expect(controller.user).to be_nil expect(response.body).to eq('') end it 'sets Current.ability when a user session is supplied' do request.env['HTTP_AUTHORIZATION'] = reader_token response = get :index_ability - expect(Current.ability).to be_an_instance_of(Ability) + expect(controller.ability).to be_an_instance_of(Ability) # if you're signed in you can download an original recording - expect(Current.ability.can?(:original, audio_recording)).to eq true + expect(controller.ability.can?(:original, audio_recording)).to eq true expect(response.body).to eq('Ability') end it 'sets Current.ability even when a user session is not supplied' do response = get :index_ability - expect(Current.ability).to be_an_instance_of(Ability) + expect(controller.ability).to be_an_instance_of(Ability) # if you're not signed in you cannot download an original recording - expect(Current.ability.can?(:original, audio_recording)).to eq false + expect(controller.ability.can?(:original, audio_recording)).to eq false expect(response.body).to eq('Ability') end diff --git a/spec/helpers/acceptance_spec_helper.rb b/spec/helpers/acceptance_spec_helper.rb index 3bb2d1c6..7f7a9984 100644 --- a/spec/helpers/acceptance_spec_helper.rb +++ b/spec/helpers/acceptance_spec_helper.rb @@ -44,7 +44,7 @@ def standard_request_options(http_method, description, expected_status, opts = { # 406 when you can't send what they want, 415 when they send what you don't want - example "#{http_method} #{description} - #{expected_status}", document: opts[:document], caller: caller do + example "#{http_method} #{description} - #{expected_status}", document: opts[:document], caller: do # allow for modification of opts, provide context so let and let! values can be accessed opts_mod&.call(self, opts) @@ -56,7 +56,7 @@ def standard_request_options(http_method, description, expected_status, opts = { raise 'Specify both expected_error_class and expected_error_regexp' if problem # remove the auth header if specified - is_remove_header = opts.dig(:remove_auth) == true + is_remove_header = opts[:remove_auth] == true header_key = 'Authorization' current_metadata = example.metadata has_header = current_metadata[:headers]&.include?(header_key) @@ -74,7 +74,7 @@ def standard_request_options(http_method, description, expected_status, opts = { opts.merge!( { - expected_status: expected_status, + expected_status:, expected_method: http_method } ) @@ -112,7 +112,7 @@ def media_request_options(http_method, description, expected_status, opts = {}) # add better metadata for tests, get the caller information that invoked this method - example "#{http_method} #{description} - #{expected_status}", document: opts[:document], caller: caller do + example "#{http_method} #{description} - #{expected_status}", document: opts[:document], caller: do audio_file = if opts[:dont_copy_test_audio] nil else @@ -124,7 +124,7 @@ def media_request_options(http_method, description, expected_status, opts = {}) opts.merge!( { - expected_status: expected_status, + expected_status:, expected_method: http_method } ) @@ -171,7 +171,7 @@ def acceptance_checks_shared(request, opts = {}) #is_documentation_run = !!(ENV['GENERATE_DOC']) actual_response = if http_method == :get && response_headers['Content-Transfer-Encoding'] == 'binary' - response_body[0...100] + ' ' + "#{response_body[0...100]} " else response_body end @@ -186,7 +186,7 @@ def acceptance_checks_shared(request, opts = {}) actual_query_string: query_string, actual_path: path, - actual_response: actual_response, + actual_response:, actual_response_has_content: !actual_response.empty?, actual_response_headers: response_headers, actual_response_content_type: response_headers['Content-Type'], @@ -199,35 +199,42 @@ def acceptance_checks_shared(request, opts = {}) } ) - opts[:msg] = "Requested #{opts[:actual_method]} #{opts[:actual_path]}. Information hash: #{MiscHelper.pretty_hash(opts)}" + opts[:msg] = + "Requested #{opts[:actual_method]} #{opts[:actual_path]}. Information hash: #{MiscHelper.pretty_hash(opts)}" # expectations expect(opts[:actual_status]).to eq(opts[:expected_status]), "Mismatch: status. #{opts[:msg]}" expect(opts[:actual_method]).to eq(opts[:expected_method]), "Mismatch: HTTP method. #{opts[:msg]}" #expect(opts[:expected_request_content_type]).to eq(opts[:actual_request_content_type]), "Mismatch: request content type. #{opts[:msg]}" - expect(opts[:actual_response_has_content]).to eq(opts[:expected_response_has_content]), "Mismatch: response has content. #{opts[:actual_response]} #{opts[:msg]}" + expect(opts[:actual_response_has_content]).to eq(opts[:expected_response_has_content]), + "Mismatch: response has content. #{opts[:actual_response]} #{opts[:msg]}" if opts[:actual_response_content_type].blank? expect(opts[:expected_response_content_type]).to be_nil, "Mismatch: response content type. #{opts[:msg]}" elsif opts.include?(:actual_response_content_type) if opts[:expected_response_content_type].nil? expect(opts[:actual_response_content_type]).to be_nil, "Mismatch: response content type. #{opts[:msg]}" else - expect(opts[:actual_response_content_type]).to include(opts[:expected_response_content_type]), "Mismatch: response content type. #{opts[:msg]}" + expect(opts[:actual_response_content_type]).to include(opts[:expected_response_content_type]), + "Mismatch: response content type. #{opts[:msg]}" end end unless opts[:actual_response_content_type].blank? if opts[:actual_response_content_type] == 'application/json' || opts[:actual_response_headers].include?('X-Error-Type') - expect(opts[:actual_response_headers]['Content-Transfer-Encoding']).to be_nil, "Mismatch: content transfer encoding. #{opts[:msg]}" - expect(opts[:actual_response_headers]['Content-Disposition']).to be_nil, "Mismatch: content disposition. #{opts[:msg]}" + expect(opts[:actual_response_headers]['Content-Transfer-Encoding']).to be_nil, + "Mismatch: content transfer encoding. #{opts[:msg]}" + expect(opts[:actual_response_headers]['Content-Disposition']).to be_nil, + "Mismatch: content disposition. #{opts[:msg]}" end if (opts[:actual_response_content_type].start_with?('image/') || opts[:actual_response_content_type].start_with?('audio/')) && !opts[:actual_response_headers].include?('X-Error-Type') - expect(opts[:actual_response_headers]['Content-Transfer-Encoding']).to eq('binary'), "Mismatch: content transfer encoding. #{opts[:msg]}" - expect(opts[:actual_response_headers]['Content-Disposition']).to match(/(inline|attachment); filename=/), "Mismatch: content disposition. #{opts[:msg]}" + expect(opts[:actual_response_headers]['Content-Transfer-Encoding']).to eq('binary'), + "Mismatch: content transfer encoding. #{opts[:msg]}" + expect(opts[:actual_response_headers]['Content-Disposition']).to match(/(inline|attachment); filename=/), + "Mismatch: content disposition. #{opts[:msg]}" end end @@ -235,12 +242,15 @@ def acceptance_checks_shared(request, opts = {}) expected_request_headers = opts[:expected_request_header_values] actual_request_headers = opts[:actual_request_headers] expected_request_headers.each do |key, value| - expect(actual_request_headers.keys).to include(key), "Mismatch: Did not find '#{key}' in request headers: #{actual_request_headers.keys.join(', ')}." - expect(actual_request_headers[key]).to eq(value), "Mismatch: Value '#{actual_request_headers[key].inspect}' for '#{key}' in request headers did not match expected value #{value.inspect}." + expect(actual_request_headers.keys).to include(key), + "Mismatch: Did not find '#{key}' in request headers: #{actual_request_headers.keys.join(', ')}." + expect(actual_request_headers[key]).to eq(value), + "Mismatch: Value '#{actual_request_headers[key].inspect}' for '#{key}' in request headers did not match expected value #{value.inspect}." end difference = actual_request_headers.keys - expected_request_headers.keys - expect(difference).to be_empty, "Mismatch: request headers differ by #{difference}: \nExpected: #{expected_request_headers} \nActual: #{actual_request_headers}" + expect(difference).to be_empty, + "Mismatch: request headers differ by #{difference}: \nExpected: #{expected_request_headers} \nActual: #{actual_request_headers}" end @@ -249,8 +259,10 @@ def acceptance_checks_shared(request, opts = {}) actual_response_headers = opts[:actual_response_headers] expected_response_headers.each do |key, value| - expect(actual_response_headers).to include(key), "Mismatch: Did not find '#{key}' in response headers: #{actual_response_headers.keys.join(', ')}." - expect(actual_response_headers[key]).to include(value), "Mismatch: Value '#{actual_response_headers[key].inspect}' for '#{key}' in response headers did not include expected value #{value.inspect}." + expect(actual_response_headers).to include(key), + "Mismatch: Did not find '#{key}' in response headers: #{actual_response_headers.keys.join(', ')}." + expect(actual_response_headers[key]).to include(value), + "Mismatch: Value '#{actual_response_headers[key].inspect}' for '#{key}' in response headers did not include expected value #{value.inspect}." end end @@ -259,13 +271,16 @@ def acceptance_checks_shared(request, opts = {}) actual_response_headers = opts[:actual_response_headers] expected_response_headers.each do |key, value| - expect(actual_response_headers).to include(key), "Mismatch: Did not find '#{key}' in response headers: #{actual_response_headers.keys.join(', ')}." - expect(actual_response_headers[key]).to eq(value), "Mismatch: Value '#{actual_response_headers[key].inspect}' for '#{key}' in response headers did not match expected value #{value.inspect}." + expect(actual_response_headers).to include(key), + "Mismatch: Did not find '#{key}' in response headers: #{actual_response_headers.keys.join(', ')}." + expect(actual_response_headers[key]).to eq(value), + "Mismatch: Value '#{actual_response_headers[key].inspect}' for '#{key}' in response headers did not match expected value #{value.inspect}." end if opts[:expected_response_header_values_match] difference = actual_response_headers.keys - expected_response_headers.keys - expect(difference).to be_empty, "Mismatch: response headers differ by #{difference}: \nExpected: #{expected_response_headers} \nActual: #{actual_response_headers}" + expect(difference).to be_empty, + "Mismatch: response headers differ by #{difference}: \nExpected: #{expected_response_headers} \nActual: #{actual_response_headers}" end end @@ -325,11 +340,13 @@ def acceptance_checks_json(opts = {}) expect(opts[:actual_response]).to be_blank, "#{message_prefix} blank response, but got #{opts[:actual_response]}" end if opts[:data_item_count].blank? - expect((data_format == :hash && data_present && actual_response_parsed_size == 1) || !data_present).to be_truthy, "#{message_prefix} no items in response, but got #{actual_response_parsed_size} items in #{opts[:actual_response]} (type #{data_format})" + expect((data_format == :hash && data_present && actual_response_parsed_size == 1) || !data_present).to be_truthy, + "#{message_prefix} no items in response, but got #{actual_response_parsed_size} items in #{opts[:actual_response]} (type #{data_format})" end unless opts[:data_item_count].blank? - expect(actual_response_parsed_size).to eq(opts[:data_item_count]), "#{message_prefix} count to be #{opts[:data_item_count]} but got #{actual_response_parsed_size} items in #{opts[:actual_response]} (type #{data_format})" + expect(actual_response_parsed_size).to eq(opts[:data_item_count]), + "#{message_prefix} count to be #{opts[:data_item_count]} but got #{actual_response_parsed_size} items in #{opts[:actual_response]} (type #{data_format})" end check_response_content(opts, message_prefix) @@ -340,7 +357,8 @@ def acceptance_checks_json(opts = {}) unless opts[:expected_json_path].blank? Array.wrap(opts[:expected_json_path]).each do |expected_json_path_item| - expect(opts[:actual_response]).to have_json_path(expected_json_path_item), "#{message_prefix} to find '#{expected_json_path_item}' in '#{opts[:actual_response]}'" + expect(opts[:actual_response]).to have_json_path(expected_json_path_item), + "#{message_prefix} to find '#{expected_json_path_item}' in '#{opts[:actual_response]}'" end end @@ -409,23 +427,28 @@ def acceptance_checks_media(opts = {}) end expect(opts[:actual_response_headers]).to include('Content-Length'), "Missing header: content length. #{opts[:msg]}" - expect(opts[:actual_response_headers]['Content-Length']).to_not be_blank, "Mismatch: content length. #{opts[:msg]}" + expect(opts[:actual_response_headers]['Content-Length']).not_to be_blank, "Mismatch: content length. #{opts[:msg]}" if is_json not_allowed_headers = MediaPoll::HEADERS_EXPOSED - ['Content-Length'] actual_present = opts[:actual_response_headers].keys - (opts[:actual_response_headers].keys - not_allowed_headers) - expect(opts[:actual_response_headers].keys).to_not include(*not_allowed_headers), "These headers were present when they should not be #{actual_present} #{opts[:msg]}" + expect(opts[:actual_response_headers].keys).not_to include(*not_allowed_headers), + "These headers were present when they should not be #{actual_present} #{opts[:msg]}" elsif opts[:actual_response_headers].keys.include?('X-Error-Type') # only use default expected headers for error if expected headers were not specified in opts - expected_headers = opts[:expected_headers] || MediaPoll::HEADERS_EXPOSED - [MediaPoll::HEADER_KEY_ELAPSED_TOTAL, MediaPoll::HEADER_KEY_ELAPSED_PROCESSING, MediaPoll::HEADER_KEY_ELAPSED_WAITING] - expect(opts[:actual_response_headers].keys).to include(*expected_headers), "Missing headers: #{expected_headers - opts[:actual_response_headers].keys} #{opts[:msg]}" + expected_headers = opts[:expected_headers] || (MediaPoll::HEADERS_EXPOSED - [MediaPoll::HEADER_KEY_ELAPSED_TOTAL, + MediaPoll::HEADER_KEY_ELAPSED_PROCESSING, MediaPoll::HEADER_KEY_ELAPSED_WAITING]) + expect(opts[:actual_response_headers].keys).to include(*expected_headers), + "Missing headers: #{expected_headers - opts[:actual_response_headers].keys} #{opts[:msg]}" else - expect(opts[:actual_response_headers].keys).to include(*MediaPoll::HEADERS_EXPOSED), "Missing headers: #{MediaPoll::HEADERS_EXPOSED - opts[:actual_response_headers].keys} #{opts[:msg]}" + expect(opts[:actual_response_headers].keys).to include(*MediaPoll::HEADERS_EXPOSED), + "Missing headers: #{MediaPoll::HEADERS_EXPOSED - opts[:actual_response_headers].keys} #{opts[:msg]}" end if opts[:is_range_request] expect(opts[:actual_response_headers]).to include('Content-Range'), "Missing header: content range. #{opts[:msg]}" - expect(opts[:actual_response_headers]['Content-Range']).to include('bytes 0-'), "Mismatch: content range. #{opts[:msg]}" + expect(opts[:actual_response_headers]['Content-Range']).to include('bytes 0-'), + "Mismatch: content range. #{opts[:msg]}" else expect(opts[:actual_response_headers]['Content-Range']).to be_nil, "Mismatch: content range. #{opts[:msg]}" end @@ -446,7 +469,7 @@ def acceptance_checks_media(opts = {}) if opts[:expected_response_has_content] expect(opts[:actual_response_headers]['Content-Length'].to_i).to eq(File.size(cache_spectrogram_possible_paths.first)), - "Mismatch: response image length. #{opts[:msg]}" + "Mismatch: response image length. #{opts[:msg]}" end elsif is_audio options[:format] = default_audio.extension @@ -457,19 +480,19 @@ def acceptance_checks_media(opts = {}) if opts[:expected_response_has_content] expect(opts[:actual_response_headers]['Content-Length'].to_i).to eq(File.size(cache_audio_possible_paths.first)), - "Mismatch: response audio length. #{opts[:msg]}" + "Mismatch: response audio length. #{opts[:msg]}" end elsif is_json expect(opts[:actual_response_headers]['Content-Length'].to_i).to be > 0, - "Mismatch: actual media json length. #{opts[:msg]}" + "Mismatch: actual media json length. #{opts[:msg]}" # TODO: files should not exist? else - raise "Unrecognised content type: #{opts[:actual_response_headers]['Content-Type']}" + raise "Unrecognized content type: #{opts[:actual_response_headers]['Content-Type']}" end else begin temp_file = File.join(Settings.paths.temp_dir, 'temp-media_controller_response') - File.open(temp_file, 'wb') { |f| f.write(response_body) } + File.binwrite(temp_file, response_body) actual_length = opts[:actual_response_headers]['Content-Length'].to_i downloaded_length = File.size(temp_file) expect(actual_length).to( @@ -483,9 +506,10 @@ def acceptance_checks_media(opts = {}) end def check_site_lat_long_response(description, expected_status, should_be_obfuscated = true) - example "#{description} - #{expected_status}", document: false, caller: caller do + example "#{description} - #{expected_status}", document: false, caller: do do_request - status.should eq(expected_status), "Requested #{path} expecting status #{expected_status} but got status #{status}. Response body was #{response_body}" + status.should eq(expected_status), + "Requested #{path} expecting status #{expected_status} but got status #{status}. Response body was #{response_body}" response_body.should have_json_path('data/location_obfuscated'), response_body.to_s #response_body.should have_json_type(Boolean).at_path('location_obfuscated'), response_body.to_s json_ = JSON.parse(response_body) @@ -513,13 +537,13 @@ def check_site_lat_long_response(description, expected_status, should_be_obfusca max = 6 expect(lat.to_s.split('.').last.size) .to satisfy { |v| v >= min && v <= max }, - "expected latitude to be obfuscated to between #{min} to #{max} places, " \ - "got #{lat.to_s.split('.').last.size} from #{lat}" + "expected latitude to be obfuscated to between #{min} to #{max} places, " \ + "got #{lat.to_s.split('.').last.size} from #{lat}" expect(long.to_s.split('.').last.size) .to satisfy { |v| v >= min && v <= max }, - "expected longitude to be obfuscated to between #{min} to #{max} places, " \ - "got #{long.to_s.split('.').last.size} from #{long}" + "expected longitude to be obfuscated to between #{min} to #{max} places, " \ + "got #{long.to_s.split('.').last.size} from #{long}" end end end @@ -530,7 +554,7 @@ def find_unexpected_entries(parent, hash, remaining_to_match, not_included) new_parent = if parent.nil? key else - parent + '/' + key + "#{parent}/#{key}" end not_included.push(new_parent) unless remaining_to_match.include?(new_parent) @@ -563,7 +587,8 @@ def check_hash_matches(expected, actual, unexpected_array_paths = []) def standard_media_parameters parameter :audio_recording_id, 'Requested audio recording id (in path/route)', required: true - parameter :format, 'Required format of the audio segment (options: json|mp3|flac|webm|ogg|wav|png). Use json if requesting metadata', required: true + parameter :format, + 'Required format of the audio segment (options: json|mp3|flac|webm|ogg|wav|png). Use json if requesting metadata', required: true parameter :start_offset, 'Start time of the audio segment in seconds' parameter :end_offset, 'End time of the audio segment in seconds' @@ -587,7 +612,7 @@ def create_media_options(audio_recording, test_audio_file = nil) options[:original_format] = File.extname(audio_recording.original_file_name) end if options[:original_format].blank? - options[:original_format] = '.' + Mime::Type.lookup(audio_recording.media_type).to_sym.to_s + options[:original_format] = ".#{Mime::Type.lookup(audio_recording.media_type).to_sym}" end options[:datetime_with_offset] = audio_recording.recorded_date options[:uuid] = audio_recording.uuid diff --git a/spec/helpers/web_server_helper.rb b/spec/helpers/web_server_helper.rb index 19411aad..6b96d4cb 100644 --- a/spec/helpers/web_server_helper.rb +++ b/spec/helpers/web_server_helper.rb @@ -30,12 +30,14 @@ def expose_app_as_web_server task.print_hierarchy(buffer) # Raise an error so it is logged: - raise TimeoutError, "Run time exceeded timeout #{timeout}s:\n#{buffer.string}" + raise Async::TimeoutError, "Run time exceeded timeout #{timeout}s:\n#{buffer.string}" } serve_task = inner.async { |inner| endpoint = Async::HTTP::Endpoint.parse(host_url) server = Falcon::Server.new( - Falcon::Server.middleware(Rails.application, verbose: true, cache: false), + # falcon logging to stderr leads to the following bug: + # https://github.com/socketry/async/issues/138 + Falcon::Server.middleware(Rails.application, verbose: false, cache: false), endpoint ) server.run diff --git a/spec/lib/gems/baw_audio_tools/audio_base/info_spec.rb b/spec/lib/gems/baw_audio_tools/audio_base/info_spec.rb index b868c473..e2029afc 100644 --- a/spec/lib/gems/baw_audio_tools/audio_base/info_spec.rb +++ b/spec/lib/gems/baw_audio_tools/audio_base/info_spec.rb @@ -27,7 +27,7 @@ it 'gives correct error for corrupt file' do expect { audio_base.info(audio_file_corrupt) - }.to raise_error(BawAudioTools::Exceptions::AudioToolError, /string=Unknown error occurred/) + }.to raise_error(BawAudioTools::Exceptions::AudioToolError, /string=Invalid data found when processing input/) end it 'returns all required information' do diff --git a/spec/lib/gems/baw_audio_tools/audio_base/integrity_check_spec.rb b/spec/lib/gems/baw_audio_tools/audio_base/integrity_check_spec.rb index a07031f6..41b271ac 100644 --- a/spec/lib/gems/baw_audio_tools/audio_base/integrity_check_spec.rb +++ b/spec/lib/gems/baw_audio_tools/audio_base/integrity_check_spec.rb @@ -132,8 +132,8 @@ expect(result[:errors]).to be_blank expect(result[:warnings].size).to be > 0 - expect(result[:warnings][0][:id]).to eq('Vorbis parser') - expect(result[:warnings][0][:description]).to eq('Invalid Setup header') + expect(result[:warnings][0][:id]).to eq('ogg') + expect(result[:warnings][0][:description]).to eq('CRC mismatch!') if result[:warnings].size > 1 expect(result[:warnings][1][:id]).to eq('ogg') diff --git a/spec/lib/gems/baw_workers/active_job/unique_spec.rb b/spec/lib/gems/baw_workers/active_job/unique_spec.rb index 589c4fe9..b7f90ef9 100644 --- a/spec/lib/gems/baw_workers/active_job/unique_spec.rb +++ b/spec/lib/gems/baw_workers/active_job/unique_spec.rb @@ -75,14 +75,6 @@ def unique_setup expect(second_job.unique?).to eq false end - it 'perform_later with a block is not allowed in application jobs' do - expect { - Fixtures::BasicJob.perform_later do - puts 'i\'m a block and i\'m ok' - end - }.to raise_error(RuntimeError, 'perform_later does not support blocks') - end - it 'fails to enqueue a job with same id (perform_later)' do result = Fixtures::BasicJob.perform_later('first_id') expect(result).to eq false diff --git a/spec/lib/gems/baw_workers/redis_communicator_spec.rb b/spec/lib/gems/baw_workers/redis_communicator_spec.rb index 05a0d00b..876ce920 100644 --- a/spec/lib/gems/baw_workers/redis_communicator_spec.rb +++ b/spec/lib/gems/baw_workers/redis_communicator_spec.rb @@ -19,7 +19,7 @@ it 'wraps keys in a namespace' do communicator.set('abc', 123) - expect(unwrapped_redis.exists('baw-workers:abc')).to eq(true) + expect(unwrapped_redis.exists?('baw-workers:abc')).to eq(true) end it 'allows the key namespace to be configurable' do @@ -31,7 +31,7 @@ communicator.set('123', 'abc') - expect(unwrapped_redis.exists('boogey-monster:123')).to eq(true) + expect(unwrapped_redis.exists?('boogey-monster:123')).to eq(true) end it 'wraps the SET command' do @@ -93,7 +93,7 @@ unwrapped_redis.set('baw-workers:my_object', '{"test":{"nested_hash":123},"string":"test"}') expect(communicator.delete('my_object')).to eq(true) - expect(unwrapped_redis.exists('baw-workers:my_object')).to eq(false) + expect(unwrapped_redis.exists?('baw-workers:my_object')).to eq(false) end it 'wraps the EXISTS command' do diff --git a/spec/lib/gems/baw_workers/upload_service/upload_service_steps.rb b/spec/lib/gems/baw_workers/upload_service/upload_service_steps.rb index 4216e833..07242c4d 100644 --- a/spec/lib/gems/baw_workers/upload_service/upload_service_steps.rb +++ b/spec/lib/gems/baw_workers/upload_service/upload_service_steps.rb @@ -46,7 +46,7 @@ module UploadServiceSteps step 'the users:' do |table| @users = table.rows.map { |(username, password)| - BawWorkers::Config.upload_communicator.create_upload_user(username: username, password: password) + BawWorkers::Config.upload_communicator.create_upload_user(username:, password:) } end @@ -71,13 +71,13 @@ module UploadServiceSteps @username = username @password = password @user = BawWorkers::Config.upload_communicator.create_upload_user( - username: username, - password: password + username:, + password: ) end step 'I :on_off the user' do |enabled| - result = BawWorkers::Config.upload_communicator.set_user_status(@user, enabled: enabled) + result = BawWorkers::Config.upload_communicator.set_user_status(@user, enabled:) expect(result).to eq(SftpgoClient::ApiResponse.new(message: 'User updated')) end @@ -89,7 +89,12 @@ module UploadServiceSteps def run_curl(command, should_work:) # upload with curl since container doesn't have scp/sftp installed and it is # not worth adding the tools for one test - output = `#{command} -v 2>&1` + # + # libssh2 (which curl uses) has some kind of bug that results in a connection + # hanging indefinitely if the server closes the connection abruptly (e.g + # in the case where auth fails). + # Use the unix timeout command to force a shutdown. + output = `timeout 6 #{command} -v 2>&1` message = lambda { "Expected exit code 0, got #{$CHILD_STATUS.exitstatus}.\nCommand: #{command}\nOutput & errpr:\n#{output}" @@ -105,7 +110,7 @@ def run_curl(command, should_work:) @upload_path = Fixtures.send(file.to_sym) run_curl( - %(curl --user "#{@username}:#{@password}" -T #{@upload_path} -k "sftp://#{@upload_host}:2022"), + %(curl --insecure --user "#{@username}:#{@password}" -T #{@upload_path} -k "sftp://#{@upload_host}:2022"), should_work: should ) end diff --git a/spec/lib/modules/filter/query_filter_encoded_spec.rb b/spec/lib/modules/filter/query_filter_encoded_spec.rb new file mode 100644 index 00000000..bc28b496 --- /dev/null +++ b/spec/lib/modules/filter/query_filter_encoded_spec.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +describe Filter::Query do + create_entire_hierarchy + + include SqlHelpers::Example + + def compare_filter_sql(filter, sql_result) + filter_query = create_filter(filter) + comparison_sql(filter_query.query_full.to_sql, sql_result) + filter_query + end + + def create_filter(params) + Filter::Query.new( + params, + AudioRecording.all, + AudioRecording, + AudioRecording.filter_settings + ) + end + + let(:test_filter) { + '{"filter":{"regions.id":{"eq":11}},"sorting":{"order_by":"recorded_date","direction":"desc"},"paging":{"items":25},"projection":{"include":["id","recorded_date","sites.name","site_id","canonical_file_name"]}}' + } + + let(:test_filter_encoded) { + # Base64.urlsafe_encode64(test_filter) + # Note: padding characters were removed as most encoders do not include them for base64url + 'eyJmaWx0ZXIiOnsicmVnaW9ucy5pZCI6eyJlcSI6MTF9fSwic29ydGluZyI6eyJvcmRlcl9ieSI6InJlY29yZGVkX2RhdGUiLCJkaXJlY3Rpb24iOiJkZXNjIn0sInBhZ2luZyI6eyJpdGVtcyI6MjV9LCJwcm9qZWN0aW9uIjp7ImluY2x1ZGUiOlsiaWQiLCJyZWNvcmRlZF9kYXRlIiwic2l0ZXMubmFtZSIsInNpdGVfaWQiLCJjYW5vbmljYWxfZmlsZV9uYW1lIl19fQ' + } + + context 'filter_encoded query string parameter' do + context 'errors' do + it 'rejects invalid base64 strings (an arbitrary string)' do + expect { + create_filter({ + filter_encoded: 'banana' + }).query_full + }.to raise_error( + CustomErrors::FilterArgumentError, 'filter_encoded was not a valid RFC 4648 base64url string' + ) + end + + it 'rejects invalid json (an arbitrary string)' do + expect { + create_filter({ + filter_encoded: '0o0o0o0o' + }).query_full + }.to raise_error( + CustomErrors::FilterArgumentError, + /filter_encoded was not a valid JSON payload: unexpected token at '.*'/ + ) + end + + it 'rejects invalid base64URL encoding' do + expect { + create_filter({ + filter_encoded: 'banana!' + }).query_full + }.to raise_error( + CustomErrors::FilterArgumentError, 'filter_encoded was not a valid RFC 4648 base64url string' + ) + end + + it 'rejects truncated values' do + expect { + create_filter({ + filter_encoded: test_filter_encoded[0..-3] + }).query_full + }.to raise_error( + CustomErrors::FilterArgumentError, + /filter_encoded was not a valid JSON payload: unexpected token at '.*'/ + ) + end + end + + context 'when decoding' do + it 'works as intended' do + params = { + filter_encoded: test_filter_encoded + } + + complex_result = <<~SQL + SELECT "audio_recordings"."id", "audio_recordings"."recorded_date", "sites"."name" + AS "sites.name", "audio_recordings"."site_id", "audio_recordings"."media_type" + FROM "audio_recordings" + LEFT + OUTER + JOIN "sites" + ON "audio_recordings"."site_id" = "sites"."id" + WHERE ("audio_recordings"."deleted_at" IS NULL) + AND ("audio_recordings"."id" + IN ( + SELECT "audio_recordings"."id" + FROM "audio_recordings" + LEFT + OUTER + JOIN "sites" + ON "audio_recordings"."site_id" = "sites"."id" + LEFT + OUTER + JOIN "regions" + ON "sites"."region_id" = "regions"."id" + WHERE "regions"."id" = 11)) + ORDER + BY "audio_recordings"."recorded_date" + DESC + LIMIT 25 + OFFSET 0 + SQL + compare_filter_sql(params, complex_result) + end + + it 'ignores an empty filter_encoded value' do + params = { + projection: { include: ['id'] }, + filter_encoded: '' + } + + complex_result = <<~SQL + SELECT "audio_recordings"."id" + FROM "audio_recordings" + WHERE "audio_recordings"."deleted_at" + IS + NULL + ORDER BY "audio_recordings"."recorded_date" + DESC + LIMIT 25 + OFFSET 0 + SQL + compare_filter_sql(params, complex_result) + end + + it 'checks filter_encoded is practically the same as filter' do + params_normal = JSON.parse(test_filter) + params_encoded = { filter_encoded: test_filter_encoded } + + comparison_sql( + create_filter(params_normal).query_full.to_sql, + create_filter(params_encoded).query_full.to_sql + ) + end + end + + context 'when merging' do + it 'ensures other query string parameters take priority' do + params = { + filter_encoded: test_filter_encoded, + items: 5, + filter_id: '1' + } + + complex_result = <<~SQL + SELECT "audio_recordings"."id", "audio_recordings"."recorded_date", "sites"."name" + AS "sites.name", "audio_recordings"."site_id", "audio_recordings"."media_type" + FROM "audio_recordings" + LEFT + OUTER + JOIN "sites" + ON "audio_recordings"."site_id" = "sites"."id" + WHERE ("audio_recordings"."deleted_at" + IS + NULL) + AND ("audio_recordings"."id" + IN ( + SELECT "audio_recordings"."id" + FROM "audio_recordings" + LEFT + OUTER + JOIN "sites" + ON "audio_recordings"."site_id" = "sites"."id" + LEFT + OUTER + JOIN "regions" + ON "sites"."region_id" = "regions"."id" + WHERE "regions"."id" = 11)) + AND ("audio_recordings"."id" = 1) + ORDER + BY "audio_recordings"."recorded_date" + DESC + LIMIT 5 + OFFSET 0 + SQL + compare_filter_sql(params, complex_result) + end + + it 'ensures other body parameters take priority' do + params = { + filter_encoded: test_filter_encoded, + filter: { id: { eq: 1 } } + } + + complex_result = <<~SQL + SELECT "audio_recordings"."id", "audio_recordings"."recorded_date", "sites"."name" + AS "sites.name", "audio_recordings"."site_id", "audio_recordings"."media_type" + FROM "audio_recordings" + LEFT + OUTER + JOIN "sites" + ON "audio_recordings"."site_id" = "sites"."id" + WHERE ("audio_recordings"."deleted_at" + IS + NULL) + AND ("audio_recordings"."id" = 1) + AND ("audio_recordings"."id" + IN ( + SELECT "audio_recordings"."id" + FROM "audio_recordings" + LEFT + OUTER + JOIN "sites" + ON "audio_recordings"."site_id" = "sites"."id" + LEFT + OUTER + JOIN "regions" + ON "sites"."region_id" = "regions"."id" + WHERE "regions"."id" = 11)) + ORDER + BY "audio_recordings"."recorded_date" + DESC + LIMIT 25 + OFFSET 0 + SQL + compare_filter_sql(params, complex_result) + end + end + end +end diff --git a/spec/lib/modules/filter/query_spec.rb b/spec/lib/modules/filter/query_spec.rb index 31dafc95..16264960 100644 --- a/spec/lib/modules/filter/query_spec.rb +++ b/spec/lib/modules/filter/query_spec.rb @@ -21,7 +21,7 @@ def create_filter(params) end # none_relation, direction asc - # unrecognised filter + # Unrecognized filter # and, or, not, other (error) # range errors (missing from/to, interval), interval outside range? context 'error' do @@ -38,7 +38,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, 'Unrecognised combiner or field name: not_a_real_filter.') + }.to raise_error(CustomErrors::FilterArgumentError, 'Unrecognized combiner or field name: not_a_real_filter.') end it 'occurs when or has only 1 entry' do @@ -135,7 +135,7 @@ def create_filter(params) } } ).query_full - }.to raise_error(CustomErrors::FilterArgumentError, /Unrecognised combiner or field name: not_a_valid_combiner/) + }.to raise_error(CustomErrors::FilterArgumentError, /Unrecognized combiner or field name: not_a_valid_combiner/) end it "occurs when a range is missing 'from'" do @@ -1130,7 +1130,7 @@ def create_filter(params) JOIN "audio_recordings" "tmp_audio_recordings_generator" ON "tmp_audio_recordings_generator"."id" IS - NULL) analysis_jobs_items + NULL) "analysis_jobs_items" INNER JOIN "audio_recordings" ON ("audio_recordings"."deleted_at" @@ -1178,13 +1178,13 @@ def create_filter(params) JOIN "audio_recordings" "tmp_audio_recordings_generator" ON "tmp_audio_recordings_generator"."id" IS - NULL) analysis_jobs_items + NULL) "analysis_jobs_items" INNER JOIN "audio_recordings" ON ("audio_recordings"."deleted_at" IS NULL) - AND ("audio_recordings"."id" = "analysis_jobs_items"."audio_recording_id")) analysis_jobs_items + AND ("audio_recordings"."id" = "analysis_jobs_items"."audio_recording_id")) "analysis_jobs_items" LEFT OUTER JOIN "audio_recordings" @@ -1448,9 +1448,9 @@ def create_filter(params) site = project.sites.first project1 = project - project2 = FactoryBot.create(:project, creator: user, sites: [site]) + project2 = create(:project, creator: user, sites: [site]) - project3 = FactoryBot.create(:project, creator: user, sites: [site]) + project3 = create(:project, creator: user, sites: [site]) request_body_obj = { projection: { @@ -1476,9 +1476,9 @@ def create_filter(params) site = project.sites.first project1 = project - project2 = FactoryBot.create(:project, creator: user, sites: [site]) + project2 = create(:project, creator: user, sites: [site]) - project3 = FactoryBot.create(:project, creator: user, sites: [site]) + project3 = create(:project, creator: user, sites: [site]) request_body_obj = { projection: { @@ -1502,7 +1502,7 @@ def create_filter(params) context 'gets projects' do it 'inaccessible' do the_user = owner_user - project_no_access = FactoryBot.create(:project, creator: no_access_user) + project_no_access = create(:project, creator: no_access_user) request_body_obj = { projection: { @@ -1525,8 +1525,8 @@ def create_filter(params) the_user = owner_user project_access = project - project_no_access = FactoryBot.create(:project) - access_via_created = FactoryBot.create(:project, creator: the_user) + project_no_access = create(:project) + access_via_created = create(:project, creator: the_user) request_body_obj = { projection: { @@ -1550,8 +1550,8 @@ def create_filter(params) it 'restricts sites to project' do the_user = owner_user - project_new = FactoryBot.create(:project, creator: the_user) - site2 = FactoryBot.create(:site, creator: the_user) + project_new = create(:project, creator: the_user) + site2 = create(:site, creator: the_user) project_new.sites << site2 project_new.save! @@ -1576,13 +1576,13 @@ def create_filter(params) it 'restricts sites to those in projects that cannot be accessed' do the_user = owner_user - project2 = FactoryBot.create(:project, creator: the_user) - site2 = FactoryBot.create(:site, creator: the_user) + project2 = create(:project, creator: the_user) + site2 = create(:site, creator: the_user) project2.sites << site2 project2.save! - project3 = FactoryBot.create(:project, creator: no_access_user) - site3 = FactoryBot.create(:site, creator: no_access_user) + project3 = create(:project, creator: no_access_user) + site3 = create(:site, creator: no_access_user) project3.sites << site3 project3.save! @@ -1605,9 +1605,9 @@ def create_filter(params) end it 'restricts permissions to project' do - the_user = FactoryBot.create(:user) - permission1 = FactoryBot.create(:read_permission, creator: the_user, user: the_user) - permission2 = FactoryBot.create(:read_permission, creator: the_user, user: the_user) + the_user = create(:user) + permission1 = create(:read_permission, creator: the_user, user: the_user) + permission2 = create(:read_permission, creator: the_user, user: the_user) project2 = permission2.project request_body_obj = { @@ -1709,13 +1709,13 @@ def create_filter(params) it 'restricts comments to audio events in projects that can not be accessed' do the_user = owner_user - comment1 = FactoryBot.create(:audio_event_comment, creator: the_user) + comment1 = create(:audio_event_comment, creator: the_user) site1 = comment1.audio_event.audio_recording.site site1.projects << project site1.save! - comment2 = FactoryBot.create(:audio_event_comment) - project2 = FactoryBot.create(:project, creator: no_access_user) + comment2 = create(:audio_event_comment) + project2 = create(:project, creator: no_access_user) site2 = comment2.audio_event.audio_recording.site site2.projects << project2 site2.save! @@ -1733,7 +1733,7 @@ def create_filter(params) filter_query_project2 = Filter::Query.new( request_body_obj, Access::ByPermission.audio_event_comments(the_user, levels: Access::Core.levels_none, - audio_event: audio_event2), + audio_event: audio_event2), AudioEventComment, AudioEventComment.filter_settings ) @@ -1754,13 +1754,13 @@ def create_filter(params) ) expect { filter.build.parse(filter.filter) - }.to raise_error(CustomErrors::FilterArgumentError, 'Unrecognised combiner or field name: this_is_not_a_field.') + }.to raise_error(CustomErrors::FilterArgumentError, 'Unrecognized combiner or field name: this_is_not_a_field.') end it 'allows mapped fields as a generic equality field' do - audio_event = FactoryBot.create( + audio_event = create( :audio_event, - audio_recording: audio_recording, + audio_recording:, start_time_seconds: 10, end_time_seconds: 88 ) @@ -1770,7 +1770,7 @@ def create_filter(params) filter = Filter::Query.new( { filter_start_time_seconds: 10, filter_end_time_seconds: 88 }, - Access::ByPermission.audio_events(admin_user, audio_recording: audio_recording), + Access::ByPermission.audio_events(admin_user, audio_recording:), AudioEvent, AudioEvent.filter_settings ) @@ -1783,9 +1783,9 @@ def create_filter(params) end it 'allows generic equality fields' do - audio_event = FactoryBot.create( + audio_event = create( :audio_event, - audio_recording: audio_recording, + audio_recording:, start_time_seconds: 10, end_time_seconds: 88 ) @@ -1795,7 +1795,7 @@ def create_filter(params) filter = Filter::Query.new( { filter_duration_seconds: 78 }, - Access::ByPermission.audio_events(admin_user, audio_recording: audio_recording), + Access::ByPermission.audio_events(admin_user, audio_recording:), AudioEvent, AudioEvent.filter_settings ) @@ -1825,9 +1825,9 @@ def create_filter(params) end it 'overrides filter parameters that match generic equality fields' do - audio_event = FactoryBot.create( + audio_event = create( :audio_event, - audio_recording: audio_recording, + audio_recording:, start_time_seconds: 10, end_time_seconds: 88 ) @@ -1837,7 +1837,7 @@ def create_filter(params) duration_seconds: { eq: 78 }, start_time_seconds: { eq: 20 } }, filter_start_time_seconds: 10, filter_end_time_seconds: 88 }, - Access::ByPermission.audio_events(admin_user, audio_recording: audio_recording), + Access::ByPermission.audio_events(admin_user, audio_recording:), AudioEvent, AudioEvent.filter_settings ) @@ -1854,7 +1854,7 @@ def create_filter(params) it 'overrides filter parameters that match text partial match field for admin' do # audio_recording needs a site, otherwise it won't be found # in by_permission#permission_sites - audio_recording = FactoryBot.create( + audio_recording = create( :audio_recording, site: Site.first, media_type: 'audio/mp3', @@ -1889,7 +1889,7 @@ def create_filter(params) it 'overrides filter parameters that match text partial match field for writer' do # audio_recording needs a site, otherwise it won't be found # in by_permission#permission_sites - audio_recording = FactoryBot.create( + audio_recording = create( :audio_recording, site: Site.first, media_type: 'audio/mp3', @@ -2001,7 +2001,7 @@ def create_filter(params) context 'project with no sites' do it 'returns no sites for admin' do filter_hash = { filter: {} } - project_id = FactoryBot.create(:project, creator: admin_user).id + project_id = create(:project, creator: admin_user).id filter_query = Access::ByPermission.sites(admin_user, project_ids: project_id) filter = Filter::Query.new( filter_hash, @@ -2018,7 +2018,7 @@ def create_filter(params) it 'returns no sites for regular user' do filter_hash = { filter: {} } - project_id = FactoryBot.create(:project, creator: writer_user).id + project_id = create(:project, creator: writer_user).id filter_query = Access::ByPermission.sites(writer_user, project_ids: project_id) filter = Filter::Query.new( filter_hash, diff --git a/spec/lib/modules/custom_render_spec.rb b/spec/lib/modules/render_markdown_spec.rb similarity index 89% rename from spec/lib/modules/custom_render_spec.rb rename to spec/lib/modules/render_markdown_spec.rb index a8a6d810..24997b03 100644 --- a/spec/lib/modules/custom_render_spec.rb +++ b/spec/lib/modules/render_markdown_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true - - describe 'rendering markdown' do let(:markdown_fixture) { <<~MARKDOWN @@ -50,15 +48,15 @@ it 'converts markdown (inline: true), strips block tags' do html = CustomRender.render_markdown(markdown_fixture, inline: true) - expect(html).to_not match '

Testy test!

' + expect(html).not_to match '

Testy test!

' expect(html).to match 'Testy test!' - expect(html).to_not match(%r{
  • a list
  • }) + expect(html).not_to match(%r{
  • a list
  • }) expect(html).to match 'a list' - expect(html).to_not match(%r{
    .*some code\n
    }) + expect(html).not_to match(%r{
    .*some code\n
    }) expect(html).to match 'some code' - expect(html).to_not match(%r{[\S\s]*[\S\s]*
    WAVE
    }) + expect(html).not_to match(%r{[\S\s]*[\S\s]*
    WAVE
    }) expect(html).to match(/WAVE\s*.wav/) - expect(html).to_not match(%r{ users.id) # describe AudioEvent, type: :model do - subject { FactoryBot.build(:audio_event) } + subject { build(:audio_event) } it 'has a valid factory' do - expect(FactoryBot.create(:audio_event)).to be_valid + expect(create(:audio_event)).to be_valid end it 'can have a blank end time' do - ae = FactoryBot.build(:audio_event, end_time_seconds: nil) + ae = build(:audio_event, end_time_seconds: nil) expect(ae).to be_valid end it 'can have a blank high frequency' do - expect(FactoryBot.build(:audio_event, high_frequency_hertz: nil)).to be_valid + expect(build(:audio_event, high_frequency_hertz: nil)).to be_valid end - it 'can have a blank end time and a blank high frequency' do - expect(FactoryBot.build(:audio_event, { end_time_seconds: nil, high_frequency_hertz: nil })).to be_valid + it 'can have a blank end time and a blank high frequency' do + expect(build(:audio_event, { end_time_seconds: nil, high_frequency_hertz: nil })).to be_valid end it { is_expected.to belong_to(:audio_recording) } @@ -84,7 +84,7 @@ end it 'has a recent scope' do - FactoryBot.create_list(:audio_event, 20) + create_list(:audio_event, 20) events = AudioEvent.most_recent(5).to_a expect(events).to have(5).items @@ -92,7 +92,7 @@ end it 'has a total duration scope' do - FactoryBot.create_list(:audio_event, 10) do |item| + create_list(:audio_event, 10) do |item| item.start_time_seconds = 0 item.end_time_seconds = 60 item.save! @@ -104,7 +104,7 @@ end it 'has a recent_within scope' do - old = FactoryBot.create(:audio_event, created_at: 2.months.ago) + old = create(:audio_event, created_at: 2.months.ago) actual = AudioEvent.created_within(1.month.ago) expect(actual.count).to eq(AudioEvent.count - 1) @@ -140,7 +140,7 @@ WHERE "projects"."deleted_at" IS NULL - AND "projects_sites"."site_id" = "sites"."id") projects, "sites"."id" + AND "projects_sites"."site_id" = "sites"."id") "projects", "sites"."id" AS "site_id", "sites"."name" AS "site_name",to_char("audio_recordings"."recorded_date" + CAST("audio_events"."start_time_seconds" || ' seconds' as interval) + @@ -167,7 +167,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'common_name') common_name_tags,( + AND "tags"."type_of_tag" = 'common_name') "common_name_tags",( SELECT string_agg( CAST("tags"."id" as varchar), '|') FROM "tags" @@ -175,7 +175,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'common_name') common_name_tag_ids,( + AND "tags"."type_of_tag" = 'common_name') "common_name_tag_ids",( SELECT string_agg( CAST("tags"."id" as varchar) || ':' || "tags"."text", '|') FROM "tags" @@ -183,7 +183,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'species_name') species_name_tags,( + AND "tags"."type_of_tag" = 'species_name') "species_name_tags",( SELECT string_agg( CAST("tags"."id" as varchar), '|') FROM "tags" @@ -191,7 +191,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'species_name') species_name_tag_ids,( + AND "tags"."type_of_tag" = 'species_name') "species_name_tag_ids",( SELECT string_agg( CAST("tags"."id" as varchar) || ':' || "tags"."text" || ':' || "tags"."type_of_tag", '|') FROM "tags" @@ -201,7 +201,7 @@ WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" AND NOT ("tags"."type_of_tag" - IN ('species_name', 'common_name'))) other_tags,( + IN ('species_name', 'common_name'))) "other_tags",( SELECT string_agg( CAST("tags"."id" as varchar), '|') FROM "tags" @@ -211,7 +211,7 @@ WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" AND NOT ("tags"."type_of_tag" - IN ('species_name', 'common_name'))) other_tag_ids,'http://localhost/listen/'|| "audio_recordings"."id" || '?start=' || (floor("audio_events"."start_time_seconds" / 30) * 30) || '&end=' || ((floor("audio_events"."start_time_seconds" / 30) * 30) + 30) + IN ('species_name', 'common_name'))) "other_tag_ids",'http://localhost/listen/'|| "audio_recordings"."id" || '?start=' || (floor("audio_events"."start_time_seconds" / 30) * 30) || '&end=' || ((floor("audio_events"."start_time_seconds" / 30) * 30) + 30) AS "listen_url",'http://localhost/library/' || "audio_recordings"."id" || '/audio_events/' || audio_events.id AS "library_url" FROM "audio_events" @@ -287,7 +287,7 @@ WHERE "projects"."deleted_at" IS NULL - AND "projects_sites"."site_id" = "sites"."id") projects, "sites"."id" + AND "projects_sites"."site_id" = "sites"."id") "projects", "sites"."id" AS "site_id", "sites"."name" AS "site_name",to_char("audio_recordings"."recorded_date" + CAST("audio_events"."start_time_seconds" || ' seconds' as interval) + @@ -314,7 +314,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'common_name') common_name_tags,( + AND "tags"."type_of_tag" = 'common_name') "common_name_tags",( SELECT string_agg( CAST("tags"."id" as varchar), '|') FROM "tags" @@ -322,7 +322,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'common_name') common_name_tag_ids,( + AND "tags"."type_of_tag" = 'common_name') "common_name_tag_ids",( SELECT string_agg( CAST("tags"."id" as varchar) || ':' || "tags"."text", '|') FROM "tags" @@ -330,7 +330,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'species_name') species_name_tags,( + AND "tags"."type_of_tag" = 'species_name') "species_name_tags",( SELECT string_agg( CAST("tags"."id" as varchar), '|') FROM "tags" @@ -338,7 +338,7 @@ JOIN "audio_events_tags" ON "audio_events_tags"."tag_id" = "tags"."id" WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" - AND "tags"."type_of_tag" = 'species_name') species_name_tag_ids,( + AND "tags"."type_of_tag" = 'species_name') "species_name_tag_ids",( SELECT string_agg( CAST("tags"."id" as varchar) || ':' || "tags"."text" || ':' || "tags"."type_of_tag", '|') FROM "tags" @@ -348,7 +348,7 @@ WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" AND NOT ("tags"."type_of_tag" - IN ('species_name', 'common_name'))) other_tags,( + IN ('species_name', 'common_name'))) "other_tags",( SELECT string_agg( CAST("tags"."id" as varchar), '|') FROM "tags" @@ -358,7 +358,7 @@ WHERE "audio_events_tags"."audio_event_id" = "audio_events"."id" AND NOT ("tags"."type_of_tag" - IN ('species_name', 'common_name'))) other_tag_ids,'http://localhost/listen/'|| "audio_recordings"."id" || '?start=' || (floor("audio_events"."start_time_seconds" / 30) * 30) || '&end=' || ((floor("audio_events"."start_time_seconds" / 30) * 30) + 30) + IN ('species_name', 'common_name'))) "other_tag_ids",'http://localhost/listen/'|| "audio_recordings"."id" || '?start=' || (floor("audio_events"."start_time_seconds" / 30) * 30) || '&end=' || ((floor("audio_events"."start_time_seconds" / 30) * 30) + 30) AS "listen_url",'http://localhost/library/' || "audio_recordings"."id" || '/audio_events/' || audio_events.id AS "library_url" FROM "audio_events" @@ -403,27 +403,27 @@ end it 'excludes deleted projects, sites, audio_recordings, and audio_events from annotation download' do - user = FactoryBot.create(:user, user_name: 'owner user checking excluding deleted items in annotation download') + user = create(:user, user_name: 'owner user checking excluding deleted items in annotation download') # create combinations of deleted and not deleted for project, site, audio_recording, audio_event expected_audio_recording = nil (0..1).each do |project_n| - project = FactoryBot.create(:project, creator: user) + project = create(:project, creator: user) project.destroy if project_n == 1 (0..1).each do |site_n| - site = FactoryBot.create(:site, :with_lat_long, creator: user) + site = create(:site, :with_lat_long, creator: user) site.projects << project site.save! site.destroy if site_n == 1 (0..1).each do |audio_recording_n| - audio_recording = FactoryBot.create(:audio_recording, :status_ready, creator: user, uploader: user, - site: site) + audio_recording = create(:audio_recording, :status_ready, creator: user, uploader: user, + site:) audio_recording.destroy if audio_recording_n == 1 (0..1).each do |audio_event_n| - audio_event = FactoryBot.create(:audio_event, creator: user, audio_recording: audio_recording) + audio_event = create(:audio_event, creator: user, audio_recording:) audio_event.destroy if audio_event_n == 1 if project_n == 0 && site_n == 0 && audio_recording_n == 0 && audio_event_n == 0 expected_audio_recording = audio_event @@ -461,22 +461,22 @@ end it 'ensures only one instance of each audio event in annotation download' do - user = FactoryBot.create(:user, user_name: 'owner user checking audio event uniqueness in annotation download') + user = create(:user, user_name: 'owner user checking audio event uniqueness in annotation download') # create 2 of everything for project, site, audio_recording, audio_event 2.times do - project = FactoryBot.create(:project, creator: user) + project = create(:project, creator: user) 2.times do - site = FactoryBot.create(:site, :with_lat_long, creator: user) + site = create(:site, :with_lat_long, creator: user) site.projects << project site.save! 2.times do - audio_recording = FactoryBot.create(:audio_recording, :status_ready, creator: user, uploader: user, - site: site) + audio_recording = create(:audio_recording, :status_ready, creator: user, uploader: user, + site:) - FactoryBot.create_list(:audio_event, 2, creator: user, audio_recording: audio_recording) + create_list(:audio_event, 2, creator: user, audio_recording:) end end end diff --git a/spec/models/audio_recording_spec.rb b/spec/models/audio_recording_spec.rb index f84388bb..0ba49345 100644 --- a/spec/models/audio_recording_spec.rb +++ b/spec/models/audio_recording_spec.rb @@ -58,36 +58,36 @@ end it 'has a valid FactoryBot factory' do - ar = FactoryBot.create(:audio_recording, + ar = create(:audio_recording, recorded_date: Time.zone.now.advance(seconds: -10), duration_seconds: Settings.audio_recording_min_duration_sec) expect(ar).to be_valid end it 'has a valid FactoryBot factory' do - ar = FactoryBot.create(:audio_recording) + ar = create(:audio_recording) expect(ar).to be_valid end it 'creating it with a nil :uuid will not generate one on validation' do # so because it is auto generated, setting :uuid to nil won't work here - expect(FactoryBot.build(:audio_recording, uuid: nil)).not_to be_valid + expect(build(:audio_recording, uuid: nil)).not_to be_valid end it 'is invalid without a uuid' do - ar = FactoryBot.create(:audio_recording) + ar = create(:audio_recording) ar.uuid = nil expect(ar.save).to be_falsey expect(ar).not_to be_valid end it 'has a uuid when created' do - ar = FactoryBot.build(:audio_recording) + ar = build(:audio_recording) expect(ar.uuid).not_to be_nil end it 'has same uuid before and after saved to db' do - ar = FactoryBot.build(:audio_recording) + ar = build(:audio_recording) uuid_before = ar.uuid expect(ar).to be_valid expect(ar.uuid).not_to be_nil @@ -98,7 +98,7 @@ end it 'fails validation when uploader is nil' do - test_item = FactoryBot.build(:audio_recording) + test_item = build(:audio_recording) test_item.uploader = nil expect(subject).not_to be_valid @@ -107,7 +107,7 @@ end it 'has a recent items scope' do - FactoryBot.create_list(:audio_recording, 20) + create_list(:audio_recording, 20) events = AudioRecording.most_recent(5).to_a expect(events).to have(5).items @@ -115,7 +115,7 @@ end it 'has a created_within scope' do - old = FactoryBot.create(:audio_recording, created_at: 2.months.ago) + old = create(:audio_recording, created_at: 2.months.ago) actual = AudioRecording.created_within(1.month.ago) expect(actual.count).to eq(AudioRecording.count - 1) @@ -124,7 +124,7 @@ it 'has a total bytes helper' do n = 0 - FactoryBot.create_list(:audio_recording, 10) do |item| + create_list(:audio_recording, 10) do |item| n += 1 item.data_length_bytes = n * 1_000_000_000 item.save! @@ -136,7 +136,7 @@ end it 'has a total duration scope' do - FactoryBot.create_list(:audio_recording, 10) do |item| + create_list(:audio_recording, 10) do |item| # field is limited to < 1M item.duration_seconds = 999_999 item.save! @@ -148,7 +148,7 @@ end context 'validation' do - subject { FactoryBot.build(:audio_recording) } + subject { build(:audio_recording) } it { is_expected.to belong_to(:creator).with_foreign_key(:creator_id) } it { is_expected.to belong_to(:updater).with_foreign_key(:updater_id).optional } @@ -208,11 +208,11 @@ context 'in same site' do it 'allows non overlapping dates - (first before second)' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:03+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:03+10:00', + site_id: 1001) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(0) @@ -224,11 +224,11 @@ end it 'allows non overlapping dates - (second before first)' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:03+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:03+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', + site_id: 1001) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(0) @@ -240,11 +240,11 @@ end it 'does not allow overlapping dates - exact' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', + site_id: 1001) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(1) @@ -261,11 +261,11 @@ end it 'does not allow overlapping dates - shift forwards' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:48+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:04+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:48+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:04+10:00', + site_id: 1001) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(1) @@ -282,11 +282,11 @@ end it 'does not allow overlapping dates - shift forwards (overlap both ends)' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 30.0, recorded_date: '2014-02-07T17:50:20+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:10+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 30.0, recorded_date: '2014-02-07T17:50:20+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:10+10:00', + site_id: 1001) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(1) @@ -303,11 +303,11 @@ end it 'does not allow overlapping dates - shift backwards' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:04+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:48+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:04+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:48+10:00', + site_id: 1001) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(1) @@ -324,11 +324,11 @@ end it 'does not allow overlapping dates - shift backwards (1 sec overlap)' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:59+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:59+10:00', + site_id: 1001) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(1) @@ -350,11 +350,11 @@ end it 'allows overlapping dates - edges exact (first before second)' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:00+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:00+10:00', + site_id: 1001) expect(ar1.recorded_date.advance(seconds: ar1.duration_seconds)).to eq(Time.zone.parse('2014-02-07T17:51:00+10:00')) expect(ar2.recorded_date.advance(seconds: ar2.duration_seconds)).to eq(Time.zone.parse('2014-02-07T17:52:00+10:00')) @@ -368,11 +368,11 @@ end it 'allows overlapping dates - edges exact (second before first)' do - site = FactoryBot.create(:site, id: 1001) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:00+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', - site_id: 1001) + site = create(:site, id: 1001) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:00+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', + site_id: 1001) expect(ar1.recorded_date.advance(seconds: ar1.duration_seconds)).to eq(Time.zone.parse('2014-02-07T17:52:00+10:00')) expect(ar2.recorded_date.advance(seconds: ar2.duration_seconds)).to eq(Time.zone.parse('2014-02-07T17:51:00+10:00')) @@ -388,12 +388,12 @@ context 'in different sites' do it 'allows overlapping dates - exact' do - FactoryBot.create(:site, id: 1001) - FactoryBot.create(:site, id: 1002) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', - site_id: 1002) + create(:site, id: 1001) + create(:site, id: 1002) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', + site_id: 1002) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(0) @@ -405,12 +405,12 @@ end it 'allows overlapping dates - shift forwards' do - FactoryBot.create(:site, id: 1001) - FactoryBot.create(:site, id: 1002) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 30.0, recorded_date: '2014-02-07T17:50:20+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:10+10:00', - site_id: 1002) + create(:site, id: 1001) + create(:site, id: 1002) + ar1 = create(:audio_recording, duration_seconds: 30.0, recorded_date: '2014-02-07T17:50:20+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:10+10:00', + site_id: 1002) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(0) @@ -422,12 +422,12 @@ end it 'allows overlapping dates - shift backwards' do - FactoryBot.create(:site, id: 1001) - FactoryBot.create(:site, id: 1002) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:30+10:00', - site_id: 1002) + create(:site, id: 1001) + create(:site, id: 1002) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:03+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:30+10:00', + site_id: 1002) result = ar2.fix_overlaps expect(result[:overlap][:count]).to eq(0) @@ -439,12 +439,12 @@ end it 'allows overlapping dates - edges exact' do - FactoryBot.create(:site, id: 1001) - FactoryBot.create(:site, id: 1002) - ar1 = FactoryBot.create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', - site_id: 1001) - ar2 = FactoryBot.build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:00+10:00', - site_id: 1002) + create(:site, id: 1001) + create(:site, id: 1002) + ar1 = create(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:50:00+10:00', + site_id: 1001) + ar2 = build(:audio_recording, duration_seconds: 60.0, recorded_date: '2014-02-07T17:51:00+10:00', + site_id: 1002) expect(ar1.recorded_date.advance(seconds: ar1.duration_seconds)).to eq(Time.zone.parse('2014-02-07T17:51:00+10:00')) expect(ar2.recorded_date.advance(seconds: ar2.duration_seconds)).to eq(Time.zone.parse('2014-02-07T17:52:00+10:00')) @@ -460,36 +460,36 @@ it 'does not allow duplicate files' do file_hash = MiscHelper.new.create_sha_256_hash('c110884206d25a83dd6d4c741861c429c10f99df9102863dde772f149387d891') - FactoryBot.create(:audio_recording, file_hash: file_hash) - expect(FactoryBot.build(:audio_recording, file_hash: file_hash)).not_to be_valid + create(:audio_recording, file_hash:) + expect(build(:audio_recording, file_hash:)).not_to be_valid end it 'does not allow audio recordings shorter than minimum duration' do expect { - FactoryBot.create(:audio_recording, duration_seconds: Settings.audio_recording_min_duration_sec - 1) + create(:audio_recording, duration_seconds: Settings.audio_recording_min_duration_sec - 1) }.to raise_error(ActiveRecord::RecordInvalid, - "Validation failed: Duration seconds must be greater than or equal to #{Settings.audio_recording_min_duration_sec}") + /Validation failed: Duration seconds must be greater than or equal to #{Settings.audio_recording_min_duration_sec}.*/) end it 'allows audio recordings equal to than minimum duration' do - ar = FactoryBot.build(:audio_recording, duration_seconds: Settings.audio_recording_min_duration_sec) + ar = build(:audio_recording, duration_seconds: Settings.audio_recording_min_duration_sec) expect(ar).to be_valid end it 'allows audio recordings longer than minimum duration' do - ar = FactoryBot.create(:audio_recording, duration_seconds: Settings.audio_recording_min_duration_sec + 1) + ar = create(:audio_recording, duration_seconds: Settings.audio_recording_min_duration_sec + 1) expect(ar).to be_valid end it 'allows data_length_bytes of more than int32 max' do - FactoryBot.create(:audio_recording, data_length_bytes: 2_147_483_648) + create(:audio_recording, data_length_bytes: 2_147_483_648) end it '(temporarily)s allow duplicate empty file hash to be updated to real hash' do - ar1 = FactoryBot.build(:audio_recording, uuid: UUIDTools::UUID.random_create.to_s, file_hash: 'SHA256::') + ar1 = build(:audio_recording, uuid: UUIDTools::UUID.random_create.to_s, file_hash: 'SHA256::') ar1.save(validate: false) - ar2 = FactoryBot.build(:audio_recording, uuid: UUIDTools::UUID.random_create.to_s, file_hash: 'SHA256::') + ar2 = build(:audio_recording, uuid: UUIDTools::UUID.random_create.to_s, file_hash: 'SHA256::') ar2.save(validate: false) ar2.file_hash = MiscHelper.new.create_sha_256_hash @@ -497,7 +497,7 @@ end it 'provides a hash split function to return the file_hash components' do - ar = FactoryBot.build(:audio_recording, file_hash: 'SHA256::abc123') + ar = build(:audio_recording, file_hash: 'SHA256::abc123') protocol, value = ar.split_file_hash @@ -505,7 +505,7 @@ expect(value).to eq('abc123') expect { - ar = FactoryBot.build(:audio_recording, file_hash: 'SHA256::abc::123') + ar = build(:audio_recording, file_hash: 'SHA256::abc::123') ar.split_file_hash }.to raise_error(RuntimeError, 'Invalid file hash detected (more than one "::" found)') end @@ -514,8 +514,8 @@ uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' date = '20180226-222930Z' - ar = FactoryBot.build(:audio_recording, uuid: uuid, recorded_date: DateTime.strptime(date, '%Y%m%d-%H%M%S%z'), - media_type: 'audio/wav') + ar = build(:audio_recording, uuid:, recorded_date: DateTime.strptime(date, '%Y%m%d-%H%M%S%z'), + media_type: 'audio/wav') actual = ar.canonical_filename @@ -524,10 +524,10 @@ it 'can return a friendly file name for an audio recording' do date = DateTime.parse('2018-02-26T22:29:30+10:00').utc - site = FactoryBot.create(:site, name: "Ant's super cool site", tzinfo_tz: 'Australia/Brisbane') - ar = FactoryBot.build( + site = create(:site, name: "Ant's super cool site", tzinfo_tz: 'Australia/Brisbane') + ar = build( :audio_recording, - site: site, + site:, id: 123_456, recorded_date: date, media_type: 'audio/wav' @@ -540,10 +540,10 @@ it 'can return a friendly file name for an audio recording (site is missing a timezone)' do date = DateTime.parse('2018-02-26T22:29:30+10:00').utc - site = FactoryBot.create(:site, name: "Ant's super cool site", tzinfo_tz: nil) - ar = FactoryBot.build( + site = create(:site, name: "Ant's super cool site", tzinfo_tz: nil) + ar = build( :audio_recording, - site: site, + site:, id: 123_456, recorded_date: date, media_type: 'audio/wav' diff --git a/spec/models/bookmark_spec.rb b/spec/models/bookmark_spec.rb index 69a3ba07..5616e7d9 100644 --- a/spec/models/bookmark_spec.rb +++ b/spec/models/bookmark_spec.rb @@ -29,7 +29,7 @@ # bookmarks_updater_id_fk (updater_id => users.id) # describe Bookmark, type: :model do - subject { FactoryBot.build(:bookmark) } + subject { build(:bookmark) } it 'has a valid factory' do expect(create(:bookmark)).to be_valid @@ -48,29 +48,32 @@ it { is_expected.to validate_presence_of(:offset_seconds) } it { is_expected.to validate_numericality_of(:offset_seconds).is_greater_than_or_equal_to(0) } + it 'is invalid without offset_seconds specified' do expect(build(:bookmark, offset_seconds: nil)).not_to be_valid end + it 'is invalid with offset_seconds set to less than zero' do expect(build(:bookmark, offset_seconds: -1)).not_to be_valid end - it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to(:creator_id).with_message('should be unique per user') } + it { + is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to(:creator_id).with_message('should be unique per user') + } - it 'should not allow duplicate names for the same user (case-insensitive)' do + it 'does not allow duplicate names for the same user (case-insensitive)' do user = create(:user) create(:bookmark, { creator: user, name: 'I love the smell of napalm in the morning.' }) ss = build(:bookmark, { creator: user, name: 'I LOVE the smell of napalm in the morning.' }) expect(ss).not_to be_valid - expect(ss.valid?).to be_falsey - expect(ss.errors[:name].size).to eq(1) + expect(ss).not_to be_valid ss.name = 'I love the smell of napalm in the morning. It smells like victory.' ss.save expect(ss).to be_valid end - it 'should allow duplicate names for different users (case-insensitive)' do + it 'allows duplicate names for different users (case-insensitive)' do user1 = create(:user) user2 = create(:user) user3 = create(:user) diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb index cb4bf89e..3235e6aa 100644 --- a/spec/models/site_spec.rb +++ b/spec/models/site_spec.rb @@ -63,22 +63,24 @@ describe Site, type: :model do it 'has a valid factory' do - expect(FactoryBot.create(:site)).to be_valid + expect(create(:site)).to be_valid end + it 'is invalid without a name' do - expect(FactoryBot.build(:site, name: nil)).not_to be_valid + expect(build(:site, name: nil)).not_to be_valid end + it 'requires a name with at least two characters' do - s = FactoryBot.build(:site, name: 's') + s = build(:site, name: 's') + expect(s).not_to be_valid expect(s).not_to be_valid - expect(s.valid?).to be_falsey expect(s.errors[:name].size).to eq(1) end - it 'should obfuscate lat/longs properly' do + it 'obfuscates lat/longs properly' do original_lat = -23.0 original_lng = 127.0 - s = FactoryBot.build(:site, :with_lat_long) + s = build(:site, :with_lat_long) jitter_range = Site::JITTER_RANGE jitter_exclude_range = Site::JITTER_RANGE * 0.1 @@ -96,15 +98,15 @@ jit_lng = Site.add_location_jitter(s.longitude, lng_min, lng_max) expect(jit_lat).to be_within(jitter_range).of(s.latitude) - expect(jit_lat).to_not be_within(jitter_exclude_range).of(s.latitude) + expect(jit_lat).not_to be_within(jitter_exclude_range).of(s.latitude) expect(jit_lng).to be_within(jitter_range).of(s.longitude) - expect(jit_lng).to_not be_within(jitter_exclude_range).of(s.longitude) + expect(jit_lng).not_to be_within(jitter_exclude_range).of(s.longitude) } end it 'latitude should be within the range [-90, 90]' do - site = FactoryBot.build(:site) + site = build(:site) latitudes.each { |value, pass| site.latitude = value @@ -115,8 +117,9 @@ end } end + it 'longitudes should be within the range [-180, 180]' do - site = FactoryBot.build(:site) + site = build(:site) longitudes.each { |value, pass| site.longitude = value @@ -127,15 +130,16 @@ end } end + it { is_expected.to have_and_belong_to_many :projects } it { is_expected.to belong_to(:region).optional } it { is_expected.to belong_to(:creator).with_foreign_key(:creator_id) } it { is_expected.to belong_to(:updater).with_foreign_key(:updater_id).optional } it { is_expected.to belong_to(:deleter).with_foreign_key(:deleter_id).optional } - it 'should error on checking orphaned site if site is orphaned' do + it 'errors on checking orphaned site if site is orphaned' do # depends on factory not automatically associating a site with any projects - site = FactoryBot.create(:site) + site = create(:site) expect { Access::Core.check_orphan_site!(site) }.to raise_error(CustomErrors::OrphanedSiteError) @@ -144,23 +148,32 @@ it 'generates html for description' do md = "# Header\r\n [a link](https://github.com)." html = "

    Header

    \n

    a link.

    \n" - site_html = FactoryBot.create(:site, description: md) + site_html = create(:site, description: md) expect(site_html.description).to eq(md) expect(site_html.description_html).to eq(html) end - it 'should error on invalid timezone' do + it 'is invalid with an invalid timezone' do + site = build(:site, tzinfo_tz: 'blah') + expect(site).not_to be_valid + end + + it 'errors on invalid timezone' do + site = create(:site) + expect(site).to be_valid + + site.tzinfo_tz = 'blah' expect { - FactoryBot.create(:site, tzinfo_tz: 'blah') + site.save! }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Tzinfo tz is not a recognized timezone ('blah')") end - it 'should be valid for a valid timezone' do - expect(FactoryBot.create(:site, tzinfo_tz: 'Australia - Brisbane')).to be_valid + it 'is valid for a valid timezone' do + expect(create(:site, tzinfo_tz: 'Australia - Brisbane')).to be_valid end - it 'should include TimeZoneAttribute' do + it 'includes TimeZoneAttribute' do expect(Site.new).to be_a_kind_of(TimeZoneAttribute) end @@ -170,7 +183,7 @@ # rejecting('text/xml', 'image_maybe/abc', 'some_image/png') } it 'has a safe_name function' do - site = FactoryBot.build(:site, name: "!aNT\'s fully s!ck site 1337 ;;\n../\\") + site = build(:site, name: "!aNT\'s fully s!ck site 1337 ;;\n../\\") expect(site.safe_name).to eq('aNTs-fully-s-ck-site-1337') end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7a756db8..e6c926a3 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -288,6 +288,8 @@ end config.before type: :request do + # If this is not set, when the controllers do redirects they will now throw unsafe redirect errors + host! "#{Settings.host.name}:#{Settings.host.port}" end config.after type: :request do diff --git a/spec/requests/application_spec.rb b/spec/requests/application_spec.rb index 323cfcaf..3ffc78eb 100644 --- a/spec/requests/application_spec.rb +++ b/spec/requests/application_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true - - describe 'Common behaviour', { type: :request } do it '404 should return 404' do headers = { @@ -10,8 +8,8 @@ get '/i_do_not_exist', params: nil, headers: headers - expect(response).to have_http_status(404) - expect(response.headers).to_not have_key('X-Error-Message') + expect(response).to have_http_status(:not_found) + expect(response.headers).not_to have_key('X-Error-Message') expect(response.content_type).to include('application/json') parsed_response = JSON.parse(response.body) expect(parsed_response['meta']['error']['details']).to eq('Could not find the requested page.') @@ -24,8 +22,84 @@ get '/images/apple-touch-icon-72x72.png', params: nil, headers: headers - expect(response).to have_http_status(404) + expect(response).to have_http_status(:not_found) expect(response.body.length).to eq 0 - expect(response.headers).to_not have_key('X-Error-Message') + expect(response.headers).not_to have_key('X-Error-Message') + end + + describe 'filtering tests' do + create_entire_hierarchy + + let(:test_filter) { + '{"filter":{"id":{"gt":0}},"sorting":{"order_by":"recorded_date","direction":"desc"},"paging":{"items":25},"projection":{"include":["id","recorded_date","sites.name","site_id","canonical_file_name"]}}' + } + + let(:test_filter_encoded) { + # Base64.urlsafe_encode64(test_filter) + # Note: padding characters were removed as most encoders do not include them for base64url + 'eyJmaWx0ZXIiOnsiaWQiOnsiZ3QiOjB9fSwic29ydGluZyI6eyJvcmRlcl9ieSI6InJlY29yZGVkX2RhdGUiLCJkaXJlY3Rpb24iOiJkZXNjIn0sInBhZ2luZyI6eyJpdGVtcyI6MjV9LCJwcm9qZWN0aW9uIjp7ImluY2x1ZGUiOlsiaWQiLCJyZWNvcmRlZF9kYXRlIiwic2l0ZXMubmFtZSIsInNpdGVfaWQiLCJjYW5vbmljYWxfZmlsZV9uYW1lIl19fQ' + } + + it 'accepts an encoded filter via a query string parameter for filter endpoints' do + url = "/audio_recordings/filter?filter_encoded=#{test_filter_encoded}" + + get url, **api_headers(reader_token) + + expect_success + expect_json_response + expect_number_of_items(1) + expect_has_projection({ include: ['id', 'recorded_date', 'sites.name', 'site_id', 'canonical_file_name'] }) + expect_has_filter({ + id: { gt: 0 } + }) + end + + it 'accepts an encoded filter via a query string parameter for index endpoints' do + url = "/audio_recordings?filter_encoded=#{test_filter_encoded}" + + get url, **api_headers(reader_token) + + expect_success + expect_json_response + expect_number_of_items(1) + expect_has_projection({ include: ['id', 'recorded_date', 'sites.name', 'site_id', 'canonical_file_name'] }) + expect_has_filter({ + id: { gt: 0 } + }) + end + + it 'accepts an encoded filter as part of a form multipart request for filter endpoints' do + body = { + filter_encoded: test_filter_encoded + } + + post '/audio_recordings/filter', params: body, **form_multipart_headers(reader_token) + + expect_success + expect_json_response + expect_number_of_items(1) + expect_has_projection({ include: ['id', 'recorded_date', 'sites.name', 'site_id', 'canonical_file_name'] }) + expect_has_filter({ + id: { gt: 0 } + }) + end + + it 'can correctly parse complex utf-8 string' do + # rubocop:disable + test = 'hello À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï ܐ ܑ ܒ ܓ ܔ ܕ ܖ ܗ ܘ ܙ ܚ ܛ ܜ ܝ ܞ ܟ' + # rubocop:enable + + filter = JSON.dump({ filter: { name: { eq: test } } }) + encoded = Base64.urlsafe_encode64(filter) + + get "/sites?filter_encoded=#{encoded}", **api_headers(reader_token) + + expect_success + expect_json_response + expect_number_of_items(0) + expect_has_filter({ + name: { eq: test } + }) + end end end diff --git a/spec/requests/audio_recordings/downloader_spec.rb b/spec/requests/audio_recordings/downloader_spec.rb index 0ee5ff48..af86fb12 100644 --- a/spec/requests/audio_recordings/downloader_spec.rb +++ b/spec/requests/audio_recordings/downloader_spec.rb @@ -130,7 +130,7 @@ def prepare_audio_file(audio_recording) prepare_audio_file audio_recording (1..10).each do |_i| - audio_recording = FactoryBot.create(:audio_recording, status: 'ready', site: site) + audio_recording = create(:audio_recording, status: 'ready', site:) prepare_audio_file audio_recording end @@ -158,7 +158,7 @@ def prepare_audio_file(audio_recording) 'curl -JO localhost:3000/audio_recordings/downloader?items=2', chdir: BawApp.tmp_dir ) - logger.info(out_and_err, status: status) + logger.info(out_and_err, status:) end script.chmod(0o764) @@ -178,7 +178,7 @@ def prepare_audio_file(audio_recording) "pwsh download_audio_files.ps1 -target downloader_test -auth_token #{auth_token}", chdir: BawApp.tmp_dir ) - logger.info(script_output, status: status) + logger.info(script_output, status:) }.run.wait! end end diff --git a/spec/requests/cms_spec.rb b/spec/requests/cms_spec.rb index 5f5e2370..5f846f1b 100644 --- a/spec/requests/cms_spec.rb +++ b/spec/requests/cms_spec.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true - - describe 'CMS' do create_standard_cms_pages prepare_users # assets should always be available - context 'allows users with no credentials to fetch assets' do +context 'when users have no credentials, they can fetch assets' do let(:page) { Comfy::Cms::Page.where(slug: 'index').first } @@ -41,11 +39,11 @@ end end - context 'admin can access the backend' do + context 'when a user is an admin they can access the backend' do example 'access admin/cms' do get '/admin/cms', headers: api_request_headers(admin_token) - expect(response).to have_http_status(302) + expect(response).to have_http_status(:found) end end end diff --git a/spec/requests/projects_spec.rb b/spec/requests/projects_spec.rb index 2edbcb44..c3b55bd3 100644 --- a/spec/requests/projects_spec.rb +++ b/spec/requests/projects_spec.rb @@ -24,7 +24,7 @@ def form_data(attributes, boundary) prepare_region let(:project_attributes) { - FactoryBot.attributes_for(:project) + attributes_for(:project) } let(:update_project_attributes) { @@ -32,7 +32,7 @@ def form_data(attributes, boundary) } let(:form_project_data) { - project_attributes = FactoryBot.attributes_for(:project) + project_attributes = attributes_for(:project) form_data(project_attributes, form_boundary) } diff --git a/spec/unit/spec/helpers/web_server_helper_spec.rb b/spec/unit/spec/helpers/web_server_helper_spec.rb index 22132a95..77ae79ac 100644 --- a/spec/unit/spec/helpers/web_server_helper_spec.rb +++ b/spec/unit/spec/helpers/web_server_helper_spec.rb @@ -26,7 +26,7 @@ def port_open? example.call raise 'Timeout did not trigger, this should not happen' - rescue Timeout::Error => e + rescue Async::TimeoutError => e logger.info('suppressing expected timeout', exception: e) true end