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