diff --git a/.rubocop.yml b/.rubocop.yml index 2fe743881..48705bad3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -83,8 +83,6 @@ Naming/PredicateName: Naming/MethodParameterName: Enabled: false -Sequel/ColumnDefault: - Enabled: false Sequel/ConcurrentIndex: Enabled: false diff --git a/Gemfile b/Gemfile index 34c01b711..a3a72f8b3 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,8 @@ source "https://rubygems.org" ruby "3.3.7" -gem "activesupport", "~> 7.2" -gem "appydays", "~> 0.7" +gem "activesupport", "~> 8.0" +gem "appydays", "~> 0.13" gem "base64" gem "bcrypt" gem "biz" @@ -12,8 +12,8 @@ gem "browser" gem "foreman" gem "frontapp" gem "geokit" -gem "grape" -gem "grape-entity" +gem "grape", "~> 2.4" +gem "grape-entity", "~> 1.0" gem "grape_logging" gem "grape-swagger" gem "holidays" @@ -33,13 +33,15 @@ gem "premailer" gem "pry" gem "pry-clipboard2" gem "puma", "~> 6.6" -gem "rack", "~> 2.2.8" +gem "rack", "~> 3.1" gem "rack-attack" -gem "rack-cors", "~> 2.0" +gem "rack-cors", "~> 3.0" gem "rack-protection" -gem "rack-ssl-enforcer" +gem "rack-session", "~> 2.1" +gem "rack-ssl-enforcer", git: "https://github.com/lithictech/rack-ssl-enforcer.git" gem "rake" gem "redcarpet" +gem "redis" gem "redis-client" gem "ruby-vips" gem "semantic_logger" @@ -52,10 +54,10 @@ gem "sequel_pg" gem "sequel-soft-deletes" gem "sequel-state-machine", "~> 1.4" gem "sequel-tstzrange-fields" -gem "sidekiq", "~> 6.5" -gem "sidekiq-amigo", ">= 1.7.0" +gem "sidekiq", "~> 8.0" +gem "sidekiq-amigo", "~> 1.11" gem "sidekiq-cron" -gem "sidekiq-unique-jobs", "~> 7.1" +gem "sidekiq-unique-jobs", "~> 8.0" gem "signalwire" gem "smstools" gem "state_machines" diff --git a/Gemfile.lock b/Gemfile.lock index 59a8bc92f..9851ff563 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,15 @@ +GIT + remote: https://github.com/lithictech/rack-ssl-enforcer.git + revision: 9dd7401d0f7a835ba05424c21d39edf5dd18d636 + specs: + rack-ssl-enforcer (0.2.9) + GEM remote: https://rubygems.org/ specs: - activesupport (7.2.0) + activesupport (8.0.2) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) @@ -12,55 +19,58 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - amazing_print (1.5.0) - appydays (0.12.2) - dotenv (~> 2.7) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + amazing_print (1.8.1) + appydays (0.13.0) + dotenv (~> 3.1) semantic_logger (~> 4.6) - ast (2.4.2) - base64 (0.2.0) - bcrypt (3.1.19) - bigdecimal (3.1.4) + ast (2.4.3) + base64 (0.3.0) + bcrypt (3.1.20) + benchmark (0.4.1) + bigdecimal (3.2.2) biz (1.8.2) clavius (~> 1.0) tzinfo - browser (5.3.1) - brpoplpush-redis_script (0.1.3) - concurrent-ruby (~> 1.0, >= 1.0.5) - redis (>= 1.0, < 6) - builder (3.2.4) + browser (6.2.0) bundle-audit (0.1.0) bundler-audit bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) clavius (1.0.4) - clipboard (1.3.6) + clipboard (1.4.1) coderay (1.1.3) - concurrent-ruby (1.3.4) - connection_pool (2.4.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) crack (1.0.0) bigdecimal rexml - css_parser (1.16.0) + cronex (0.15.0) + tzinfo + unicode (>= 0.4.4.5) + css_parser (1.21.1) addressable - csv (3.3.0) - diff-lcs (1.5.0) - docile (1.4.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - drb (2.2.1) - dry-core (1.0.1) + csv (3.3.5) + diff-lcs (1.6.2) + docile (1.4.1) + domain_name (0.6.20240107) + dotenv (3.1.8) + drb (2.2.3) + dry-core (1.1.0) concurrent-ruby (~> 1.0) + logger zeitwerk (~> 2.6) - dry-inflector (1.0.0) - dry-logic (1.5.0) + dry-inflector (1.2.0) + dry-logic (1.6.0) + bigdecimal concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) + dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-types (1.7.1) + dry-types (1.8.3) + bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) @@ -70,175 +80,188 @@ GEM et-orbi (1.2.11) tzinfo eventmachine (1.2.7) - excon (0.102.0) - faker (3.2.1) + excon (1.2.7) + logger + faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (2.7.10) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - faye-websocket (0.11.3) + faraday (2.13.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.1) + net-http (>= 0.5.0) + faye-websocket (0.12.0) eventmachine (>= 0.12.0) - websocket-driver (>= 0.5.1) - ffi (1.15.5) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) + websocket-driver (>= 0.8.0) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi-compiler (1.3.2) + ffi (>= 1.15.5) rake fluent_fixtures (0.11.0) faker (~> 3.2) inflecto (~> 0.0) loggability (~> 0.17) - foreman (0.87.2) - frontapp (0.0.12) + foreman (0.88.1) + frontapp (0.0.13) http (>= 2.2.1) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) geokit (1.14.0) - globalid (1.2.0) + globalid (1.2.1) + activesupport (>= 6.1) + grape (2.4.0) activesupport (>= 6.1) - grape (1.8.0) - activesupport (>= 5) - builder dry-types (>= 1.1) - mustermann-grape (~> 1.0.0) - rack (>= 1.3.0) - rack-accept - grape-entity (1.0.0) + mustermann-grape (~> 1.1.0) + rack (>= 2) + zeitwerk + grape-entity (1.0.1) activesupport (>= 3.0.0) multi_json (>= 1.3.2) - grape-swagger (1.6.1) - grape (~> 1.3) + grape-swagger (2.1.2) + grape (>= 1.7, < 3.0) + rack-test (~> 2) grape_logging (1.8.4) grape rack has-guarded-handlers (1.6.3) - hashdiff (1.1.1) - heroics (0.1.2) + hashdiff (1.2.0) + heroics (0.1.3) + base64 erubis (~> 2.0) excon moneta multi_json (>= 1.9.2) webrick - holidays (8.6.0) + holidays (8.8.0) htmlentities (4.3.4) - http (5.1.1) + http (5.3.1) addressable (~> 2.8) http-cookie (~> 1.0) http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) + llhttp-ffi (~> 0.5.0) + http-cookie (1.0.8) domain_name (~> 0.5) http-form_data (2.3.0) - httparty (0.22.0) + httparty (0.23.1) csv mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.1) + i18n (1.14.7) concurrent-ruby (~> 1.0) - image_processing (1.12.2) - mini_magick (>= 4.9.5, < 5) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) inflecto (0.0.2) - json (2.7.2) - jwt (2.7.1) - language_server-protocol (3.17.0.3) - liquid (5.4.0) - llhttp-ffi (0.4.0) + json (2.12.2) + jwt (2.10.1) + base64 + language_server-protocol (3.17.0.5) + liquid (5.8.7) + bigdecimal + strscan (>= 3.1.1) + llhttp-ffi (0.5.1) ffi-compiler (~> 1.0) rake (~> 13.0) loggability (0.18.2) - logger (1.6.0) - method_source (1.0.0) + logger (1.7.0) + method_source (1.1.0) mimemagic (0.4.3) nokogiri (~> 1) rake - mini_magick (4.12.0) + mini_magick (5.2.0) + benchmark + logger mini_mime (1.1.5) - mini_portile2 (2.8.8) - minitest (5.25.1) + minitest (5.25.5) moneta (1.0.0) - monetize (1.12.0) + monetize (1.13.0) money (~> 6.12) - money (6.16.0) + money (6.19.0) i18n (>= 0.6.4, <= 2) multi_json (1.15.0) - multi_xml (0.7.1) + multi_xml (0.7.2) bigdecimal (~> 3.1) - mustermann (3.0.0) + mustermann (3.0.3) ruby2_keywords (~> 0.0.1) - mustermann-grape (1.0.2) + mustermann-grape (1.1.0) mustermann (>= 1.0.0) - nio4r (2.7.3) - nokogiri (1.18.6) - mini_portile2 (~> 2.8.2) + net-http (0.6.0) + uri + nio4r (2.7.4) + nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.6-x86_64-darwin) + nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.6-x86_64-linux-gnu) - racc (~> 1.4) - parallel (1.26.3) - parser (3.3.4.2) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc - pg (1.5.4) + pg (1.5.9) pgvector (0.3.2) - phony (2.20.7) - platform-api (3.5.0) + phony (2.22.2) + platform-api (3.8.0) heroics (~> 0.1.1) moneta (~> 1.0.0) rate_throttle_client (~> 0.1.0) - premailer (1.21.0) + premailer (1.27.0) addressable - css_parser (>= 1.12.0) + css_parser (>= 1.19.0) htmlentities (>= 4.0.0) - pry (0.14.2) + prism (1.4.0) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) pry-clipboard2 (1.0.0) clipboard (~> 1) pry (~> 0) - public_suffix (5.0.3) + public_suffix (6.0.2) puma (6.6.0) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.13) - rack-accept (0.4.5) - rack (>= 0.4) + rack (3.1.16) rack-attack (6.7.0) rack (>= 1.0, < 4) - rack-cors (2.0.2) - rack (>= 2.0.0) - rack-protection (3.1.0) - rack (~> 2.2, >= 2.2.4) - rack-ssl-enforcer (0.2.9) - rack-test (2.1.0) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) rack (>= 1.3) rainbow (3.1.1) - rake (13.0.6) + rake (13.3.0) rate_throttle_client (0.1.2) - redcarpet (3.6.0) - redis (4.8.1) - redis-client (0.16.0) + redcarpet (3.6.1) + redis (5.4.0) + redis-client (>= 0.22.0) + redis-client (0.25.0) connection_pool - regexp_parser (2.9.2) + regexp_parser (2.10.0) rexml (3.4.1) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) rspec-eventually (0.2.2) - rspec-expectations (3.12.3) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) + rspec-support (~> 3.13.0) rspec-json_expectations (2.2.0) - rspec-mocks (3.12.6) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.1) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) rspec-temp_dir (1.1.1) rspec (>= 3.0) rubocop (1.65.1) @@ -252,29 +275,31 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.1) - parser (>= 3.3.1.0) - rubocop-performance (1.21.1) + rubocop-ast (1.45.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.23.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-sequel (0.3.4) + rubocop-sequel (0.3.8) rubocop (~> 1.0) ruby-progressbar (1.13.0) - ruby-vips (2.1.4) + ruby-vips (2.2.4) ffi (~> 1.12) + logger ruby2_keywords (0.0.5) - securerandom (0.3.1) - semantic_logger (4.14.0) + securerandom (0.4.1) + semantic_logger (4.16.1) concurrent-ruby (~> 1.0) - sentry-ruby (5.19.0) + sentry-ruby (5.25.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.19.0) - sentry-ruby (~> 5.19.0) + sentry-sidekiq (5.25.0) + sentry-ruby (~> 5.25.0) sidekiq (>= 3.0) - sequel (5.75.0) + sequel (5.93.0) bigdecimal sequel-annotate (1.7.0) sequel (>= 4) @@ -287,26 +312,27 @@ GEM sequel-tstzrange-fields (0.2.1) pg sequel - sequel_pg (1.17.1) + sequel_pg (1.17.2) pg (>= 0.18.0, != 1.2.0) sequel (>= 4.38.0) - sidekiq (6.5.12) - connection_pool (>= 2.2.5, < 3) - rack (~> 2.0) - redis (>= 4.5.0, < 5) - sidekiq-amigo (1.8.0) - sidekiq (~> 6) - sidekiq-cron (~> 1) - sidekiq-cron (1.10.1) - fugit (~> 1.8) + sidekiq (8.0.4) + connection_pool (>= 2.5.0) + json (>= 2.9.0) + logger (>= 1.6.2) + rack (>= 3.1.0) + redis-client (>= 0.23.2) + sidekiq-amigo (1.11.0) + sidekiq (>= 7) + sidekiq-cron (~> 2) + sidekiq-cron (2.3.0) + cronex (>= 0.13.0) + fugit (~> 1.8, >= 1.11.1) globalid (>= 1.0.1) - sidekiq (>= 6) - sidekiq-unique-jobs (7.1.33) - brpoplpush-redis_script (> 0.1.1, <= 2.0.0) + sidekiq (>= 6.5.0) + sidekiq-unique-jobs (8.0.11) concurrent-ruby (~> 1.0, >= 1.0.5) - redis (< 5.0) - sidekiq (>= 5.0, < 7.0) - thor (>= 0.20, < 3.0) + sidekiq (>= 7.0.0, < 9.0.0) + thor (>= 1.0, < 3.0) signalwire (2.5.0) concurrent-ruby (~> 1.1) faye-websocket (~> 0.11) @@ -320,44 +346,43 @@ GEM simplecov-cobertura (2.1.0) rexml simplecov (~> 0.19) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) smstools (0.2.2) - state_machines (0.6.0) - stripe (9.1.0) - thor (1.3.1) - timecop (0.9.8) + state_machines (0.30.0) + stripe (15.2.1) + strscan (3.1.5) + thor (1.3.2) + timecop (0.9.10) twilio-ruby (5.77.0) faraday (>= 0.9, < 3.0) jwt (>= 1.5, < 3.0) nokogiri (>= 1.6, < 2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.5.0) - webmock (3.23.1) + unicode (0.4.4.5) + unicode-display_width (2.6.0) + uri (1.0.3) + webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.1) - websocket-driver (0.7.6) + webrick (1.9.1) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) yajl-ruby (1.4.3) - zeitwerk (2.6.11) + zeitwerk (2.7.3) PLATFORMS - ruby - x86_64-darwin-21 + arm64-darwin-23 x86_64-linux - x86_86-linux DEPENDENCIES - activesupport (~> 7.2) + activesupport (~> 8.0) amazing_print - appydays (~> 0.7) + appydays (~> 0.13) base64 bcrypt biz @@ -369,8 +394,8 @@ DEPENDENCIES foreman frontapp geokit - grape - grape-entity + grape (~> 2.4) + grape-entity (~> 1.0) grape-swagger grape_logging holidays @@ -390,14 +415,16 @@ DEPENDENCIES pry pry-clipboard2 puma (~> 6.6) - rack (~> 2.2.8) + rack (~> 3.1) rack-attack - rack-cors (~> 2.0) + rack-cors (~> 3.0) rack-protection - rack-ssl-enforcer + rack-session (~> 2.1) + rack-ssl-enforcer! rack-test rake redcarpet + redis redis-client rspec rspec-eventually @@ -418,10 +445,10 @@ DEPENDENCIES sequel-state-machine (~> 1.4) sequel-tstzrange-fields sequel_pg - sidekiq (~> 6.5) - sidekiq-amigo (>= 1.7.0) + sidekiq (~> 8.0) + sidekiq-amigo (~> 1.11) sidekiq-cron - sidekiq-unique-jobs (~> 7.1) + sidekiq-unique-jobs (~> 8.0) signalwire simplecov simplecov-cobertura diff --git a/db/migrations/021_gbfs.rb b/db/migrations/021_gbfs.rb index 12cc3b5af..e545e9c4a 100644 --- a/db/migrations/021_gbfs.rb +++ b/db/migrations/021_gbfs.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Sequel.migration do - change do + up do from(:mobility_restricted_areas).delete alter_table(:mobility_restricted_areas) do add_column :multipolygon, "decimal[][][][]", null: false diff --git a/db/migrations/031_supported_currencies_maximum_cents.rb b/db/migrations/031_supported_currencies_maximum_cents.rb index c7e175056..3b86185a7 100644 --- a/db/migrations/031_supported_currencies_maximum_cents.rb +++ b/db/migrations/031_supported_currencies_maximum_cents.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Sequel.migration do - change do + up do alter_table(:supported_currencies) do add_column :funding_maximum_cents, :integer, null: true end diff --git a/db/migrations/042_ledger_account_notnull.rb b/db/migrations/042_ledger_account_notnull.rb index aed4d80ad..78d282394 100644 --- a/db/migrations/042_ledger_account_notnull.rb +++ b/db/migrations/042_ledger_account_notnull.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Sequel.migration do - change do + up do alter_table(:payment_ledgers) do set_column_not_null :account_id end diff --git a/db/migrations/065_vector_search2.rb b/db/migrations/065_vector_search2.rb index 18d5a2097..aa81d299c 100644 --- a/db/migrations/065_vector_search2.rb +++ b/db/migrations/065_vector_search2.rb @@ -7,6 +7,7 @@ :organization_memberships, :program_enrollments, ] + # rubocop:disable Sequel/IrreversibleMigration: change do tables.each do |tbl| alter_table(tbl) do @@ -19,4 +20,5 @@ end end end + # rubocop:enable Sequel/IrreversibleMigration end diff --git a/lib/rack/spa_rewrite.rb b/lib/rack/spa_rewrite.rb index d07fd5f03..d895134b1 100644 --- a/lib/rack/spa_rewrite.rb +++ b/lib/rack/spa_rewrite.rb @@ -63,9 +63,9 @@ def get(env) @index_bytes = ::File.read(@index_path) if @index_bytes.nil? || @index_mtime < lastmodhttp headers = { - "Content-Length" => @index_bytes.bytesize, - "Content-Type" => "text/html", - "Last-Modified" => lastmodhttp, + Rack::CONTENT_LENGTH => @index_bytes.bytesize, + Rack::CONTENT_TYPE => "text/html", + "last-modified" => lastmodhttp, } return [200, headers, [@index_bytes]] end diff --git a/lib/suma.rb b/lib/suma.rb index 04fdfc9dd..08f292998 100644 --- a/lib/suma.rb +++ b/lib/suma.rb @@ -234,5 +234,6 @@ def self.as_ary(x) = x.respond_to?(:to_ary) ? x : [x] require "suma/phone_number" require "suma/typed_struct" -raise "Remove this code, ActiveSupport has the new default" unless ActiveSupport::VERSION::MAJOR <= 7 -ActiveSupport.to_time_preserves_timezone = true +raise "Remove this code, ActiveSupport has the new default" if + ActiveSupport::VERSION::MAJOR >= 8 && ActiveSupport::VERSION::MINOR >= 1 +ActiveSupport.to_time_preserves_timezone = :zone diff --git a/lib/suma/api/auth.rb b/lib/suma/api/auth.rb index 926c751db..30e013e82 100644 --- a/lib/suma/api/auth.rb +++ b/lib/suma/api/auth.rb @@ -21,7 +21,11 @@ def self.extract_phone_from_request(request) rescue JSON::ParserError return nil end - request.body.rewind + if request.body.instance_of?(::Rack::Lint::Wrapper::InputWrapper) + request.body.instance_variable_get(:@input).rewind + else + request.body.rewind + end phone = Suma::PhoneNumber::US.normalize(params["phone"]) return Suma::PhoneNumber::US.valid_normalized?(phone) ? phone : nil end diff --git a/lib/suma/async.rb b/lib/suma/async.rb index 91baa5e47..1559104e3 100644 --- a/lib/suma/async.rb +++ b/lib/suma/async.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "amigo" -require "redis" require "appydays/configurable" require "appydays/loggable" require "sentry-sidekiq" @@ -50,6 +49,40 @@ module Suma::Async "suma/async/stripe_refunds_backfiller", ].freeze + class << self + def configure_sidekiq_server(config) + url = Suma::Redis.fetch_url(self.sidekiq_redis_provider, self.sidekiq_redis_url) + redis_params = Suma::Redis.conn_params(url) + config.redis = redis_params + config[:job_logger] = Suma::Async::JobLogger + + # We do NOT want the unstructured default error handler + config.error_handlers.replace([Suma::Async::JobLogger.method(:error_handler)]) + # We must then replace the otherwise-automatically-added sentry middleware + config.error_handlers << Sentry::Sidekiq::ErrorHandler.new + + config.death_handlers << Suma::Async::JobLogger.method(:death_handler) + + config.client_middleware do |chain| + chain.add(SidekiqUniqueJobs::Middleware::Client) + end + config.server_middleware do |chain| + chain.add(SidekiqUniqueJobs::Middleware::Server) + end + + SidekiqUniqueJobs::Server.configure(config) + end + + def configure_sidekiq_client(config) + url = Suma::Redis.fetch_url(self.sidekiq_redis_provider, self.sidekiq_redis_url) + redis_params = Suma::Redis.conn_params(url) + config.redis = redis_params + config.client_middleware do |chain| + chain.add(SidekiqUniqueJobs::Middleware::Client) + end + end + end + configurable(:async) do # The number of (Float) seconds that should be considered "slow" for a job. # Jobs that take longer than this amount of time will be logged @@ -69,36 +102,11 @@ module Suma::Async setting :web_password, SecureRandom.hex(8) after_configured do - # Very hard to to test this, so it's not tested. - url = Suma::Redis.fetch_url(self.sidekiq_redis_provider, self.sidekiq_redis_url) - redis_params = Suma::Redis.conn_params(url) - Sidekiq.configure_server do |config| - config.redis = redis_params - config.options[:job_logger] = Suma::Async::JobLogger - - # We do NOT want the unstructured default error handler - config.error_handlers.replace([Suma::Async::JobLogger.method(:error_handler)]) - # We must then replace the otherwise-automatically-added sentry middleware - config.error_handlers << Sentry::Sidekiq::ErrorHandler.new - - config.death_handlers << Suma::Async::JobLogger.method(:death_handler) - - config.client_middleware do |chain| - chain.add(SidekiqUniqueJobs::Middleware::Client) - end - config.server_middleware do |chain| - chain.add(SidekiqUniqueJobs::Middleware::Server) - end - - SidekiqUniqueJobs::Server.configure(config) - end - - Sidekiq.configure_client do |config| - config.redis = redis_params - config.client_middleware do |chain| - chain.add(SidekiqUniqueJobs::Middleware::Client) - end - end + # Set this here since we need it for tests, which don't run as a real server. + # Otherwise we could put it in the configure_server call. + Sidekiq.default_configuration.logger = self.logger + Sidekiq.configure_server { |cfg| self.configure_sidekiq_server(cfg) } + Sidekiq.configure_client { |cfg| self.configure_sidekiq_client(cfg) } SidekiqUniqueJobs.configure do |config| config.logger = Appydays::Loggable[SidekiqUniqueJobs] diff --git a/lib/suma/liquid/expose.rb b/lib/suma/liquid/expose.rb index d2ba569db..591aae454 100644 --- a/lib/suma/liquid/expose.rb +++ b/lib/suma/liquid/expose.rb @@ -26,4 +26,4 @@ def render(context) end end -Liquid::Template.register_tag("expose", Suma::Liquid::Expose) +Liquid::Environment.default.register_tag("expose", Suma::Liquid::Expose) diff --git a/lib/suma/liquid/filters.rb b/lib/suma/liquid/filters.rb index 805bdcb8a..89d0bbffd 100644 --- a/lib/suma/liquid/filters.rb +++ b/lib/suma/liquid/filters.rb @@ -17,4 +17,4 @@ def card(input) end end -Liquid::Template.register_filter(Suma::Liquid::Filters) +Liquid::Environment.default.register_filter(Suma::Liquid::Filters) diff --git a/lib/suma/liquid/partial.rb b/lib/suma/liquid/partial.rb index df9be86c0..c637e5db8 100644 --- a/lib/suma/liquid/partial.rb +++ b/lib/suma/liquid/partial.rb @@ -9,4 +9,4 @@ def initialize(tag_name, name, options) super end end -Liquid::Template.register_tag("partial", Suma::Liquid::Partial) +Liquid::Environment.default.register_tag("partial", Suma::Liquid::Partial) diff --git a/lib/suma/marketing/sms_broadcast.rb b/lib/suma/marketing/sms_broadcast.rb index f066d2e8b..3177ada3e 100644 --- a/lib/suma/marketing/sms_broadcast.rb +++ b/lib/suma/marketing/sms_broadcast.rb @@ -136,7 +136,7 @@ def generate_post_review failed_recipients:, canceled_recipients:, pending_recipients: self.sms_dispatches.count - delivered_recipients - failed_recipients - canceled_recipients, - actual_cost: sw_payloads.sum(BigDecimal("0")) { |d| d.fetch("price", 0) }, + actual_cost: sw_payloads.sum(BigDecimal(0)) { |d| d.fetch("price", 0) }, ) return result end @@ -181,9 +181,9 @@ def _defaults total_recipients: 0, en_recipients: 0, es_recipients: 0, - total_cost: BigDecimal("0"), - en_total_cost: BigDecimal("0"), - es_total_cost: BigDecimal("0"), + total_cost: BigDecimal(0), + en_total_cost: BigDecimal(0), + es_total_cost: BigDecimal(0), } end end diff --git a/lib/suma/message.rb b/lib/suma/message.rb index da28ef00b..5d8130e88 100644 --- a/lib/suma/message.rb +++ b/lib/suma/message.rb @@ -23,8 +23,8 @@ class UndeliverableRecipient < Error; end configurable(:messages) do after_configured do - Liquid::Template.error_mode = :strict - Liquid::Template.file_system = Liquid::LocalFileSystem.new(DATA_DIR, "%s.liquid") + Liquid::Environment.default.error_mode = :strict + Liquid::Environment.default.file_system = Liquid::LocalFileSystem.new(DATA_DIR, "%s.liquid") end end diff --git a/lib/suma/mobility.rb b/lib/suma/mobility.rb index 3f3a497a8..37b1bc697 100644 --- a/lib/suma/mobility.rb +++ b/lib/suma/mobility.rb @@ -7,7 +7,7 @@ class OutOfBounds < ArgumentError; end # to get an integer coordinate? COORD2INT_FACTOR = 10_000_000 # Convert an integer coordinate back to a float. - INT2COORD_FACTOR = BigDecimal("1") / COORD2INT_FACTOR + INT2COORD_FACTOR = BigDecimal(1) / COORD2INT_FACTOR COORD_RANGE = -180.0..180.0 INTCOORD_RANGE = (-180.0 * COORD2INT_FACTOR)..(180.0 * COORD2INT_FACTOR) # This 'magnitude' is in lat/lng degrees/minutes. It is not an actual diff --git a/lib/suma/postgres/model_utilities.rb b/lib/suma/postgres/model_utilities.rb index c07a1aed1..f2cedeca5 100644 --- a/lib/suma/postgres/model_utilities.rb +++ b/lib/suma/postgres/model_utilities.rb @@ -24,7 +24,9 @@ def self.extended(model_class) end module ClassMethods - def named_descendants = self.descendants.reject(&:anonymous?) + def named_descendants + self.descendants.reject(&:anonymous?).reject { |cls| cls.name.start_with?("Sequel::_Model") } + end # Set up some things on new database connections. def db=(newdb) diff --git a/lib/suma/rack_attack.rb b/lib/suma/rack_attack.rb index 8fe5f742b..ac3a8d58f 100644 --- a/lib/suma/rack_attack.rb +++ b/lib/suma/rack_attack.rb @@ -33,7 +33,7 @@ module Suma::RackAttack match_data = req.env["rack.attack.match_data"] now = Time.now.to_i retry_after = match_data[:period] - (now % match_data[:period]) - headers = {"Content-Type" => "application/json", "Retry-After" => retry_after.to_s} + headers = {Rack::CONTENT_TYPE => "application/json", "retry-after" => retry_after.to_s} # Pass the retry-after value in the body as well as the header. body = Suma::Service.error_body( 429, diff --git a/lib/suma/sentry.rb b/lib/suma/sentry.rb index 4db021a2d..87eac5039 100644 --- a/lib/suma/sentry.rb +++ b/lib/suma/sentry.rb @@ -20,7 +20,7 @@ module Suma::Sentry Sentry.init do |config| # See https://docs.sentry.io/clients/ruby/config/ for more info. config.dsn = dsn - config.logger = self.logger + config.sdk_logger = self.logger end else Sentry.instance_variable_set(:@main_hub, nil) diff --git a/lib/suma/service.rb b/lib/suma/service.rb index 1b344bf60..209125345 100644 --- a/lib/suma/service.rb +++ b/lib/suma/service.rb @@ -82,8 +82,13 @@ def self.cookie_config def self.decode_cookie(s) cfg = self.cookie_config + s = s.split(";").first s = s.delete_prefix(cfg[:key] + "=") - return cfg[:coder].decode(Rack::Utils.unescape(s)) + s = Rack::Utils.unescape(s) + cookie_app = Rack::Session::Cookie.new(nil, cfg) + dc = cookie_app.encryptors.first + ds = dc.decrypt(s) + return ds end ### Build the Rack app according to the configured environment. @@ -183,6 +188,7 @@ def self.error_body(status, message, code: nil, more: {}) 409, "Attempting to lock the resource failed. You should fetch a new version of the resource and try again.", code: "lock_failed", + skip_loc_check: true, ) end @@ -191,6 +197,7 @@ def self.error_body(status, message, code: nil, more: {}) 409, "Member is in read-only mode and cannot be updated: #{e.reason}", code: e.reason, + skip_loc_check: true, ) end @@ -217,7 +224,7 @@ def self.error_body(status, message, code: nil, more: {}) msg = "An internal error occurred of type #{error_signature}. Error ID: #{error_id}" end Suma::Service.logger.error("api_exception", {error_id:, error_signature:}, e) - merror!(status, msg, code: "api_error", more:) + merror!(status, msg, code: "api_error", more:, skip_loc_check: true) end finally do diff --git a/lib/suma/service/helpers.rb b/lib/suma/service/helpers.rb index 813c9c49a..db285bc74 100644 --- a/lib/suma/service/helpers.rb +++ b/lib/suma/service/helpers.rb @@ -100,7 +100,7 @@ def merror!(status, message, code:, more: {}, skip_loc_check: false) if !skip_loc_check && Suma::Service.localized_error_codes && !Suma::Service.localized_error_codes.include?(code) merror!(500, "Error code is unlocalized: #{code}", code: "unhandled_error") end - header "Content-Type", "application/json" + header Rack::CONTENT_TYPE, "application/json" body = Suma::Service.error_body(status, message, code:, more:) error!(body, status) end diff --git a/lib/suma/service/middleware.rb b/lib/suma/service/middleware.rb index d3d07289a..b3bcd79d1 100644 --- a/lib/suma/service/middleware.rb +++ b/lib/suma/service/middleware.rb @@ -3,6 +3,7 @@ require "rack/cors" require "rack/protection" require "rack/remote_ip" +require "rack/session" require "rack/ssl-enforcer" require "sentry-ruby" require "appydays/loggable/request_logger" @@ -50,7 +51,6 @@ def self.add_cors_middleware(builder) def self.add_common_middleware(builder) builder.use(Rack::ContentLength) - builder.use(Rack::Chunked) builder.use(Rack::Deflater) builder.use(Sentry::Rack::CaptureExceptions) builder.use(Rack::RemoteIp) diff --git a/lib/suma/spec_helpers.rb b/lib/suma/spec_helpers.rb index 521cadf9f..6c7fd2b07 100644 --- a/lib/suma/spec_helpers.rb +++ b/lib/suma/spec_helpers.rb @@ -56,15 +56,15 @@ def self.included(context) respbody = body || load_fixture_data(path, raw: true) case format when :json - headers["Content-Type"] = "application/json" + headers[Rack::CONTENT_TYPE] = "application/json" when :xml - headers["Content-Type"] = "application/xml" + headers[Rack::CONTENT_TYPE] = "application/xml" end return {status:, body: respbody, headers:} end module_function def json_response(body={}, status: 200, headers: {}) - headers["Content-Type"] = "application/json" + headers[Rack::CONTENT_TYPE] = "application/json" body = body.to_json return {status:, body:, headers:} end diff --git a/lib/suma/spec_helpers/service.rb b/lib/suma/spec_helpers/service.rb index 2976457c7..51a70b86e 100644 --- a/lib/suma/spec_helpers/service.rb +++ b/lib/suma/spec_helpers/service.rb @@ -334,7 +334,7 @@ def failure_message @msg = "expected response Set-Cookie '#{cookie}' to start with #{cookie_prefix}" break false end - payload = Suma::Service.decode_cookie(cookie) + payload = Suma::Service.decode_cookie(cookie) || {} (@payload_keys || []).each do |k| break false unless payload.key?(k) end diff --git a/lib/suma/sse.rb b/lib/suma/sse.rb index f1c945a92..8e1dd4fcc 100644 --- a/lib/suma/sse.rb +++ b/lib/suma/sse.rb @@ -2,6 +2,7 @@ require "appydays/configurable" require "appydays/loggable" +require "redis_client" module Suma::SSE include Appydays::Configurable @@ -9,6 +10,7 @@ module Suma::SSE TOKEN_HEADER = "Suma-Events-Token" ORGANIZATION_MEMBERSHIP_VERIFICATIONS = "organization_membership_verifications" + NEXT_EVENT_TIMEOUT = 10 class << self attr_accessor :publisher_redis @@ -20,7 +22,7 @@ class << self after_configured do redis_url = Suma::Redis.fetch_url(self.redis_provider, self.redis_url) - self.publisher_redis = Redis.new(**Suma::Redis.conn_params(redis_url)) + self.publisher_redis = RedisClient.new(**Suma::Redis.conn_params(redis_url)) end end @@ -41,29 +43,34 @@ def publish(topic, payload, t: Time.now) if (sid = self.current_session_id) msg[:sid] = sid end - self.publisher_redis.publish(topic, msg.to_json) + self.publisher_redis.pubsub.call("PUBLISH", topic, msg.to_json) end def new_subscriber_redis redis_url = Suma::Redis.fetch_url(self.redis_provider, self.redis_url) - return Redis.new(**Suma::Redis.conn_params(redis_url)) + return RedisClient.new(**Suma::Redis.conn_params(redis_url)) end def subscribe(topic, session_id: nil) redis = self.new_subscriber_redis - redis.subscribe(topic) do |on| - on.message do |_channel, data| - msg = JSON.parse(data) - msg_sid = msg["sid"] - # The subscriber should know about the message if: - # - We don't have a subscriber - # - The message was published by an anonymous subscriber - # - The message was published by another subscriber - subscriber_cares = session_id.nil? || - msg_sid.nil? || - session_id != msg_sid - yield(msg) if subscriber_cares - end + sub = redis.pubsub + sub.call("SUBSCRIBE", topic) + loop do + event = sub.next_event(NEXT_EVENT_TIMEOUT) + next unless event + event_action, event_topic, event_data = event + next unless event_action == "message" + next unless event_topic == topic + msg = JSON.parse(event_data) + msg_sid = msg["sid"] + # The subscriber should know about the message if: + # - We don't have a subscriber + # - The message was published by an anonymous subscriber + # - The message was published by another subscriber + subscriber_cares = session_id.nil? || + msg_sid.nil? || + session_id != msg_sid + yield(msg) if subscriber_cares end rescue IOError # client disconnected @@ -74,7 +81,7 @@ def subscribe(topic, session_id: nil) class NotFound def call(*) - [404, {"Content-Type" => "text/plain"}, "Not Found"] + [404, {Rack::CONTENT_TYPE => "text/plain"}, "Not Found"] end end end diff --git a/lib/suma/sse/middleware.rb b/lib/suma/sse/middleware.rb index 4488a868a..615376f54 100644 --- a/lib/suma/sse/middleware.rb +++ b/lib/suma/sse/middleware.rb @@ -10,10 +10,10 @@ class Suma::SSE::Middleware include Appydays::Loggable HEADERS = { - "Content-Type" => "text/event-stream", - "Cache-Control" => "no-cache", - "Connection" => "keep-alive", - "Access-Control-Allow-Origin" => "*", # This is fine for our purposes + Rack::CONTENT_TYPE => "text/event-stream", + Rack::CACHE_CONTROL => "no-cache", + "connection" => "keep-alive", + "access-control-allow-origin" => "*", # This is fine for our purposes }.freeze class << self @@ -42,7 +42,7 @@ def call(env) return @app.call unless env["PATH_INFO"] == @path token = Rack::Request.new(env).GET["token"] - return [401, {"Content-Type" => "text/plain"}, "Unauthorized"] unless + return [401, {Rack::CONTENT_TYPE => "text/plain"}, "Unauthorized"] unless Suma::SSE::Auth.validate_token(token) # We must use the socket directly so we disconnect as soon as a write fails. diff --git a/lib/suma/yosoy.rb b/lib/suma/yosoy.rb index c5d0ba82e..7a00d2ae6 100644 --- a/lib/suma/yosoy.rb +++ b/lib/suma/yosoy.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "rack/session" + # Bare-bones Rack-based authentication library. # After a decade of using Warden and Grape we decided to just write our auth system for our needs. # This just manages authentication, not authorization, @@ -103,7 +105,7 @@ def call(env) def response(status_code, extra={}) headers = { - "Content-Type" => "application/json", + Rack::CONTENT_TYPE => "application/json", } body = {error: {status: status_code, **extra}} return [ diff --git a/spec/rack/immutable_spec.rb b/spec/rack/immutable_spec.rb index 15ba19b2e..a68f73d2b 100644 --- a/spec/rack/immutable_spec.rb +++ b/spec/rack/immutable_spec.rb @@ -8,7 +8,7 @@ it "sets cache-control immutable for requests that match the matcher" do mw = described_class.new(app, match: "/x") expect(mw.call(Rack::MockRequest.env_for("/x"))).to eq( - [200, {"Cache-Control" => "public, max-age=604800, immutable"}, "success"], + [200, {"cache-control" => "public, max-age=604800, immutable"}, "success"], ) end @@ -20,17 +20,17 @@ it "can match against a string" do mw = described_class.new(app, match: "/x") expect(mw.call(Rack::MockRequest.env_for("/x"))).to eq( - [200, {"Cache-Control" => "public, max-age=604800, immutable"}, "success"], + [200, {"cache-control" => "public, max-age=604800, immutable"}, "success"], ) end it "defaults match to regex matching SHA fingerprints" do mw = described_class.new(app) expect(mw.call(Rack::MockRequest.env_for("/static/foo.abcd1234.js"))).to eq( - [200, {"Cache-Control" => "public, max-age=604800, immutable"}, "success"], + [200, {"cache-control" => "public, max-age=604800, immutable"}, "success"], ) expect(mw.call(Rack::MockRequest.env_for("/static/foo.bar.abcd1234.js"))).to eq( - [200, {"Cache-Control" => "public, max-age=604800, immutable"}, "success"], + [200, {"cache-control" => "public, max-age=604800, immutable"}, "success"], ) expect(mw.call(Rack::MockRequest.env_for("/static/foo.js"))).to eq([200, {}, "success"]) expect(mw.call(Rack::MockRequest.env_for("/static/abcd1234.js"))).to eq([200, {}, "success"]) @@ -40,7 +40,7 @@ it "can match against a callable" do mw = described_class.new(app, match: ->(env) { env["PATH_INFO"] == "/xy" }) expect(mw.call(Rack::MockRequest.env_for("/xy"))).to eq( - [200, {"Cache-Control" => "public, max-age=604800, immutable"}, "success"], + [200, {"cache-control" => "public, max-age=604800, immutable"}, "success"], ) expect(mw.call(Rack::MockRequest.env_for("/x"))).to eq([200, {}, "success"]) end diff --git a/spec/rack/spa_rewrite_spec.rb b/spec/rack/spa_rewrite_spec.rb index d35c0fcd1..9ad28f482 100644 --- a/spec/rack/spa_rewrite_spec.rb +++ b/spec/rack/spa_rewrite_spec.rb @@ -36,7 +36,7 @@ expect(mw.call(Rack::MockRequest.env_for("/w", method: :get))).to eq( [ 200, - {"Content-Length" => 13, "Content-Type" => "text/html", "Last-Modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, + {"content-length" => 13, "content-type" => "text/html", "last-modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, [""], ], ) @@ -47,7 +47,7 @@ expect(mw.call(Rack::MockRequest.env_for("/w", method: :head))).to match_array( [ 200, - {"Content-Length" => 13, "Content-Type" => "text/html", "Last-Modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, + {"content-length" => 13, "content-type" => "text/html", "last-modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, be_empty, ], ) @@ -56,7 +56,7 @@ it "handles OPTIONs" do mw = described_class.new(app, index_path:, html_only: false) expect(mw.call(Rack::MockRequest.env_for("/w", method: :options))).to eq( - [200, {"Allow" => "GET, HEAD, OPTIONS", "Content-Length" => "0"}, []], + [200, {"Allow" => "GET, HEAD, OPTIONS", "content-length" => "0"}, []], ) end @@ -72,7 +72,7 @@ expect(mw.call(env)).to eq( [ 200, - {"Content-Length" => 13, "Content-Type" => "text/html", "Last-Modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, + {"content-length" => 13, "content-type" => "text/html", "last-modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, [""], ], ) @@ -89,7 +89,7 @@ expect(mw.call(Rack::MockRequest.env_for("/w.html", method: :get))).to eq( [ 200, - {"Content-Length" => 13, "Content-Type" => "text/html", "Last-Modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, + {"content-length" => 13, "content-type" => "text/html", "last-modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, [""], ], ) @@ -103,7 +103,7 @@ expect(mw.call(Rack::MockRequest.env_for("/w", method: :get))).to eq( [ 200, - {"Content-Length" => 13, "Content-Type" => "text/html", "Last-Modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, + {"content-length" => 13, "content-type" => "text/html", "last-modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, [""], ], ) @@ -113,7 +113,7 @@ expect(mw.call(Rack::MockRequest.env_for("/w.html", method: :get))).to eq( [ 200, - {"Content-Length" => 13, "Content-Type" => "text/html", "Last-Modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, + {"content-length" => 13, "content-type" => "text/html", "last-modified" => "Sun, 30 Oct 2022 00:00:00 GMT"}, [""], ], ) diff --git a/spec/suma/admin_api/auth_spec.rb b/spec/suma/admin_api/auth_spec.rb index fac26f3f2..528be8e23 100644 --- a/spec/suma/admin_api/auth_spec.rb +++ b/spec/suma/admin_api/auth_spec.rb @@ -98,7 +98,7 @@ delete "/v1/auth" expect(last_response).to have_status(204) - expect(last_response["Set-Cookie"]).to include("=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00") + expect(last_response["Set-Cookie"]).to include("=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00") expect(last_response["Clear-Site-Data"]).to eq("*") expect(admin.sessions_dataset.last).to be_logged_out end diff --git a/spec/suma/api/auth_spec.rb b/spec/suma/api/auth_spec.rb index 65c796901..afa99f9c1 100644 --- a/spec/suma/api/auth_spec.rb +++ b/spec/suma/api/auth_spec.rb @@ -270,7 +270,7 @@ delete "/v1/auth" expect(last_response).to have_status(204) - expect(last_response["Set-Cookie"]).to include("=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00") + expect(last_response["Set-Cookie"]).to include("=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00") expect(last_response["Clear-Site-Data"]).to eq("*") end end @@ -283,7 +283,7 @@ delete "/v1/auth" expect(last_response).to have_status(204) - expect(last_response["Set-Cookie"]).to include("=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00") + expect(last_response["Set-Cookie"]).to include("=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00") expect(last_response["Clear-Site-Data"]).to eq("*") expect(session.refresh).to be_logged_out end @@ -362,4 +362,21 @@ end end end + + describe "extract_phone_from_request" do + reqcls = Struct.new(:body) + + it "reads 'phone' from the body" do + body = StringIO.new({phone: "15552223333"}.to_json) + expect(described_class.extract_phone_from_request(reqcls.new(body:))).to eq("15552223333") + expect(body.pos).to eq(0) + end + + it "handles a lint wrapper" do + body = StringIO.new({phone: "15552223333"}.to_json) + wbody = Rack::Lint::Wrapper::InputWrapper.new(body) + expect(described_class.extract_phone_from_request(reqcls.new(body: wbody))).to eq("15552223333") + expect(body.pos).to eq(0) + end + end end diff --git a/spec/suma/api_spec.rb b/spec/suma/api_spec.rb index 7d5632281..086e1ff83 100644 --- a/spec/suma/api_spec.rb +++ b/spec/suma/api_spec.rb @@ -59,7 +59,7 @@ class Suma::API::TestV1API < Suma::API::V1 post "/v1/call_stripe" - expect(req).to have_been_made + expect(req).to have_been_made.times(3) expect(last_response).to have_status(500) expect(last_response).to have_json_body.that_includes(error: include(code: "api_error")) end diff --git a/spec/suma/async_spec.rb b/spec/suma/async_spec.rb index 6dafdfa8d..e292719b4 100644 --- a/spec/suma/async_spec.rb +++ b/spec/suma/async_spec.rb @@ -12,7 +12,24 @@ describe "JobLogger" do it "returns configured slow seconds" do - expect(Suma::Async::JobLogger.new.method(:slow_job_seconds).call).to eq(1) + expect(Suma::Async::JobLogger.new(Sidekiq::Config.new).method(:slow_job_seconds).call).to eq(1) + end + end + + describe "configuration" do + it "can configure the Sidekiq server" do + cfg = Sidekiq::Config.new + described_class.configure_sidekiq_server(cfg) + expect(cfg.error_handlers).to have_length(2) + expect(cfg.death_handlers).to have_length(2) + expect(cfg[:job_logger]).to eq(Suma::Async::JobLogger) + expect(cfg.instance_variable_get(:@redis_config)).to eq({url: "redis://localhost:22007/0"}) + end + + it "can configure the Sidekiq client" do + cfg = Sidekiq::Config.new + described_class.configure_sidekiq_client(cfg) + expect(cfg.instance_variable_get(:@redis_config)).to eq({url: "redis://localhost:22007/0"}) end end end diff --git a/spec/suma/message/forwarder_spec.rb b/spec/suma/message/forwarder_spec.rb index b62d20034..c9238a283 100644 --- a/spec/suma/message/forwarder_spec.rb +++ b/spec/suma/message/forwarder_spec.rb @@ -8,10 +8,12 @@ Suma::Message::Forwarder.front_inbox_id = "1234" end + let(:june14) { Time.at(1_749_921_156) } + def messagerow(swid, data={}) data[:to] ||= Suma::PhoneNumber.format_e164(Suma::Message::Forwarder.phone_numbers.sample) data[:from] ||= "+14445551234" - data[:date_created] ||= Time.now + data[:date_created] ||= june14 data[:num_media] ||= 0 r = { signalwire_id: swid, @@ -27,9 +29,9 @@ def messagerow(swid, data={}) def insert_message(swid, data={}) = Suma::Webhookdb.signalwire_messages_dataset.insert(messagerow(swid, data)) it "syncs recent messages sent to the configured numbers into the configured Front inbox" do - old = insert_message("msg2", date_created: Time.at(1_749_920_264) - 8.days) + old = insert_message("msg2", date_created: june14 - 8.days) wrong_to = insert_message("msg3", to: "+13334445555") - msg1 = insert_message("msg1", body: "hello", date_created: Time.at(1_749_921_156)) + msg1 = insert_message("msg1", body: "hello") req = stub_request(:post, "https://api2.frontapp.com/inboxes/inb_ya/imported_messages"). with(body: { @@ -43,17 +45,17 @@ def insert_message(swid, data={}) = Suma::Webhookdb.signalwire_messages_dataset. attachments: [], }.to_json).to_return(json_response({})) - described_class.new(now: Time.now).run + described_class.new(now: june14).run expect(req).to have_been_made end it "is idempotent" do - insert_message("msg1", body: "hello", date_created: Time.at(1_749_921_156)) + insert_message("msg1", body: "hello") req = stub_request(:post, "https://api2.frontapp.com/inboxes/inb_ya/imported_messages"). to_return(json_response({})) - described_class.new(now: Time.now).run - described_class.new(now: Time.now).run + described_class.new(now: june14).run + described_class.new(now: june14).run expect(req).to have_been_made.once end @@ -61,7 +63,6 @@ def insert_message(swid, data={}) = Suma::Webhookdb.signalwire_messages_dataset. msg = insert_message( "msg1", body: "hello", - date_created: Time.at(1_749_921_156), num_media: 3, subresource_uris: { media: "/api/laml/2010-04-01/Accounts/AC123/Messages/msg1/Media.json", @@ -111,7 +112,7 @@ def insert_message(swid, data={}) = Suma::Webhookdb.signalwire_messages_dataset. end.to_return(json_response({message_uid: "FMID2"}, status: 202)) # rubocop:enable Layout/LineLength - described_class.new(now: Time.now).run + described_class.new(now: june14).run expect(media_list_req).to have_been_made expect(media1_req).to have_been_made expect(media2_req).to have_been_made @@ -122,7 +123,7 @@ def insert_message(swid, data={}) = Suma::Webhookdb.signalwire_messages_dataset. it "errors if the Front inbox is not set" do described_class.front_inbox_id = "" expect do - described_class.new(now: Time.now).run + described_class.new(now: june14).run end.to raise_error(/must be set/) end end diff --git a/spec/suma/postgres/model_spec.rb b/spec/suma/postgres/model_spec.rb index d195367ee..bc5916920 100755 --- a/spec/suma/postgres/model_spec.rb +++ b/spec/suma/postgres/model_spec.rb @@ -95,6 +95,11 @@ module SumaTestModels; end expect(ds.reduce_expr(:|, [nil, false], method: :exclude)).to equal(ds) end + it "knows named descendants" do + desc = Suma::Postgres::Model.named_descendants.map(&:name) + expect(desc).to all(start_with("Suma::")) + end + describe "#find_or_create_or_find" do let(:model_class) { Suma::Postgres::TestingPixie } diff --git a/spec/suma/spec_helpers_spec.rb b/spec/suma/spec_helpers_spec.rb new file mode 100644 index 000000000..0d4a5e3af --- /dev/null +++ b/spec/suma/spec_helpers_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe Suma::SpecHelpers do + describe "fixture_response" do + it "sets headers based on the format" do + resp = fixture_response(body: "{}", format: :json, headers: {"x" => "1"}) + expect(resp).to eq( + {body: "{}", headers: {"content-type" => "application/json", "x" => "1"}, status: 200}, + ) + + resp = fixture_response(body: "", format: :xml, headers: {"x" => "1"}) + expect(resp).to eq( + {body: "", headers: {"content-type" => "application/xml", "x" => "1"}, status: 200}, + ) + end + end +end diff --git a/spec/suma/sse_spec.rb b/spec/suma/sse_spec.rb index 562fde35b..6b9b13d3f 100644 --- a/spec/suma/sse_spec.rb +++ b/spec/suma/sse_spec.rb @@ -146,10 +146,10 @@ def close expect(sock.flushes).to be_positive expect(sock.written.gsub("\r\n", "\n")).to start_with(<<~HTTP) HTTP/1.1 200 OK - Content-Type: text/event-stream - Cache-Control: no-cache - Connection: keep-alive - Access-Control-Allow-Origin: * + content-type: text/event-stream + cache-control: no-cache + connection: keep-alive + access-control-allow-origin: * HTTP expect(sock.written).to include(": keep-alive\n\n") expect(sock.written).to include('data: {"payload":{"x":1},') @@ -157,7 +157,7 @@ def close end it "closes the socket if Redis errors" do - expect(Suma::SSE).to receive(:subscribe).and_raise(Redis::CannotConnectError) + expect(Suma::SSE).to receive(:subscribe).and_raise(RedisClient::CannotConnectError) expect(app.call(env).first).to eq(-1) sleep(1) # Wait for thread to set up expect(sock.closed).to be(true) diff --git a/spec/suma/yosoy_spec.rb b/spec/suma/yosoy_spec.rb index 7fb564dc2..f919fc9d6 100644 --- a/spec/suma/yosoy_spec.rb +++ b/spec/suma/yosoy_spec.rb @@ -17,17 +17,23 @@ def req = Rack::MockRequest.env_for("/") let(:auth_obj) { {id: 1} } let(:coder) { Rack::Session::Cookie::Base64::Marshal.new } + define_method :create_cookie_app do |app| + Rack::Session::Cookie.new(app, {secret: "sekret" * 11, coder:}) + end + define_method :create_mw do |cls: mw_class, app: nil, &block| app ||= block - app = Rack::Session::Cookie.new(app, {secret: "sekret", coder:}) + app = create_cookie_app(app) app = cls.new(app) app end define_method :decode_cookie do |resp| - s = resp[1]["Set-Cookie"] + s = resp[1]["set-cookie"] s = s.delete_prefix("rack.session=") - return coder.decode(Rack::Utils.unescape(s)) + s = s.split(";", 2).first + s = Rack::Utils.unescape(s) + create_cookie_app(nil).encryptors.first.decrypt(s).to_a end it "handles the auth flow successfully" do @@ -41,7 +47,7 @@ def req = Rack::MockRequest.env_for("/") resp = mw.call(req) expect(resp).to match_array( - [200, {"Set-Cookie" => start_with("rack.session=")}, "ok"], + [200, {"set-cookie" => start_with("rack.session=")}, "ok"], ) expect(decode_cookie(resp)).to contain_exactly( ["session_id", be_present], @@ -58,7 +64,7 @@ def req = Rack::MockRequest.env_for("/") end expect(mw.call(req)).to match_array( - [401, {"Content-Type" => "application/json"}, ["{\"error\":{\"status\":401,\"code\":\"unauthenticated\"}}"]], + [401, {"content-type" => "application/json"}, ["{\"error\":{\"status\":401,\"code\":\"unauthenticated\"}}"]], ) end @@ -72,7 +78,7 @@ def req = Rack::MockRequest.env_for("/") resp = mw.call(req) expect(resp).to match_array( - [200, {"Set-Cookie" => start_with("rack.session=")}, "ok"], + [200, {"set-cookie" => start_with("rack.session=")}, "ok"], ) expect(decode_cookie(resp)).to contain_exactly( ["session_id", be_present], @@ -87,7 +93,7 @@ def req = Rack::MockRequest.env_for("/") resp = mw.call(req) expect(resp).to match_array( - [402, {"Content-Type" => "application/json"}, ["{\"error\":{\"status\":402,\"x\":1}}"]], + [402, {"content-type" => "application/json"}, ["{\"error\":{\"status\":402,\"x\":1}}"]], ) end @@ -98,7 +104,7 @@ def req = Rack::MockRequest.env_for("/") resp = mw.call(req) expect(resp).to match_array( - [402, {"Content-Type" => "application/json"}, ["{\"error\":{\"status\":402}}"]], + [402, {"content-type" => "application/json"}, ["{\"error\":{\"status\":402}}"]], ) end @@ -109,7 +115,7 @@ def req = Rack::MockRequest.env_for("/") resp = mw.call(req) expect(resp).to match_array( - [401, {"Content-Type" => "application/json"}, ["{\"error\":{\"status\":401,\"code\":\"unauthenticated\"}}"]], + [401, {"content-type" => "application/json"}, ["{\"error\":{\"status\":401,\"code\":\"unauthenticated\"}}"]], ) end @@ -140,14 +146,14 @@ def inactivity_timeout = 300 Timecop.freeze(now + 299.seconds) do env = req - env["HTTP_COOKIE"] = resp_t0[1].fetch("Set-Cookie") + env["HTTP_COOKIE"] = resp_t0[1].fetch("set-cookie") resp_t299 = mw.call(env) expect(resp_t299[0]).to eq(200) end Timecop.freeze(now + 301.seconds) do env = req - env["HTTP_COOKIE"] = resp_t0[1].fetch("Set-Cookie") + env["HTTP_COOKIE"] = resp_t0[1].fetch("set-cookie") resp_t301 = mw.call(env) expect(resp_t301[0]).to eq(401) end @@ -164,7 +170,7 @@ def inactivity_timeout = 300 Timecop.freeze(5.years.from_now) do env = req - env["HTTP_COOKIE"] = resp_t0[1].fetch("Set-Cookie") + env["HTTP_COOKIE"] = resp_t0[1].fetch("set-cookie") resp_tfuture = mw.call(env) expect(resp_tfuture[0]).to eq(200) end