diff --git a/.ruby-version b/.ruby-version index f989260..7bcbb38 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.4 +3.4.9 diff --git a/.tool-versions b/.tool-versions index 146e604..53ab124 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -ruby 3.4.4 +ruby 3.4.9 yarn 1.22.19 diff --git a/Gemfile b/Gemfile index d8ffc95..a525a56 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.4.4' -gem 'rails', '~> 7.2.2', '>= 7.2.2.1' +ruby '3.4.9' +gem 'rails', '~> 7.2.3', '>= 7.2.3' gem 'activeadmin' gem 'base64' @@ -13,7 +13,7 @@ gem 'devise' gem 'drb' gem 'importmap-rails' gem 'jbuilder', '~> 2.7' -gem 'lsa_tdx_feedback', '~> 1.0.1' +gem 'lsa_tdx_feedback', '~> 1.0', '>= 1.0.4' gem 'pg' gem 'puma', '~> 6.0' gem 'sd_notify' diff --git a/Gemfile.lock b/Gemfile.lock index 4786f68..ec994b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,47 +1,49 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + actioncable (7.2.3) + actionpack (= 7.2.3) + activesupport (= 7.2.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailbox (7.2.3) + actionpack (= 7.2.3) + activejob (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) mail (>= 2.8.0) - actionmailer (7.2.2.2) - actionpack (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailer (7.2.3) + actionpack (= 7.2.3) + actionview (= 7.2.3) + activejob (= 7.2.3) + activesupport (= 7.2.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.2.2) - actionview (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionpack (7.2.3) + actionview (= 7.2.3) + activesupport (= 7.2.3) + cgi nokogiri (>= 1.8.5) racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4, < 3.3) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.2.2) - actionpack (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actiontext (7.2.3) + actionpack (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.2.2) - activesupport (= 7.2.2.2) + actionview (7.2.3) + activesupport (= 7.2.3) builder (~> 3.1) + cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) @@ -55,22 +57,22 @@ GEM kaminari (>= 1.2.1) railties (>= 6.1) ransack (>= 4.0) - activejob (7.2.2.2) - activesupport (= 7.2.2.2) + activejob (7.2.3) + activesupport (= 7.2.3) globalid (>= 0.3.6) - activemodel (7.2.2.2) - activesupport (= 7.2.2.2) - activerecord (7.2.2.2) - activemodel (= 7.2.2.2) - activesupport (= 7.2.2.2) + activemodel (7.2.3) + activesupport (= 7.2.3) + activerecord (7.2.3) + activemodel (= 7.2.3) + activesupport (= 7.2.3) timeout (>= 0.4.0) - activestorage (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activesupport (= 7.2.2.2) + activestorage (7.2.3) + actionpack (= 7.2.3) + activejob (= 7.2.3) + activerecord (= 7.2.3) + activesupport (= 7.2.3) marcel (~> 1.0) - activesupport (7.2.2.2) + activesupport (7.2.3) base64 benchmark (>= 0.3) bigdecimal @@ -93,8 +95,8 @@ GEM ast (2.4.3) base64 (0.3.0) bcrypt (3.1.20) - benchmark (0.4.1) - bigdecimal (3.2.3) + benchmark (0.5.0) + bigdecimal (4.1.0) bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) @@ -109,11 +111,12 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cgi (0.5.1) childprocess (5.1.0) logger (~> 1.5) coderay (1.1.3) - concurrent-ruby (1.3.5) - connection_pool (2.5.4) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) crack (1.0.0) bigdecimal rexml @@ -126,7 +129,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.1) + date (3.5.1) devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -136,7 +139,7 @@ GEM diff-lcs (1.6.2) docile (1.4.1) drb (2.2.3) - erb (5.0.2) + erb (6.0.2) erubi (1.13.1) factory_bot (6.5.4) activesupport (>= 6.1.0) @@ -185,11 +188,11 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) hashdiff (1.2.0) - httparty (0.23.1) + httparty (0.24.2) csv mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.7) + i18n (1.14.8) concurrent-ruby (~> 1.0) importmap-rails (2.2.0) actionpack (>= 6.0.0) @@ -200,9 +203,10 @@ GEM has_scope (>= 0.6) railties (>= 6.0) responders (>= 2) - io-console (0.8.1) - irb (1.15.2) + io-console (0.8.2) + irb (1.17.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.13.0) @@ -242,14 +246,15 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.24.1) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - lsa_tdx_feedback (1.0.1) - httparty (~> 0.22) + lsa_tdx_feedback (1.0.4) + httparty (~> 0.24.0) rails (>= 6.0) redis (>= 4.0) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop @@ -258,11 +263,13 @@ GEM matrix (0.4.3) method_source (1.1.0) mini_mime (1.1.5) - minitest (5.25.5) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) msgpack (1.8.0) - multi_xml (0.7.2) - bigdecimal (~> 3.1) - net-imap (0.5.10) + multi_xml (0.8.1) + bigdecimal (>= 3.1, < 5) + net-imap (0.6.3) date net-protocol net-pop (0.1.2) @@ -271,22 +278,22 @@ GEM timeout net-smtp (0.5.1) net-protocol - nio4r (2.7.4) - nokogiri (1.18.10-aarch64-linux-gnu) + nio4r (2.7.5) + nokogiri (1.19.2-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.10-aarch64-linux-musl) + nokogiri (1.19.2-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.10-arm-linux-gnu) + nokogiri (1.19.2-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.10-arm-linux-musl) + nokogiri (1.19.2-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.10-arm64-darwin) + nokogiri (1.19.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.10-x86_64-darwin) + nokogiri (1.19.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.10-x86_64-linux-gnu) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.10-x86_64-linux-musl) + nokogiri (1.19.2-x86_64-linux-musl) racc (~> 1.4) orm_adapter (0.5.0) parallel (1.27.0) @@ -294,7 +301,7 @@ GEM ast (~> 2.4.1) racc pg (1.5.9) - pp (0.6.2) + pp (0.6.3) prettyprint prettier_print (1.2.1) prettyprint (0.2.0) @@ -307,35 +314,35 @@ GEM pry (>= 0.13, < 0.16) pry-rails (0.3.11) pry (>= 0.13.0) - psych (5.2.6) + psych (5.3.1) date stringio public_suffix (6.0.2) puma (6.6.1) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.16) + rack (3.2.5) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) - rackup (2.2.1) + rackup (2.3.1) rack (>= 3) - rails (7.2.2.2) - actioncable (= 7.2.2.2) - actionmailbox (= 7.2.2.2) - actionmailer (= 7.2.2.2) - actionpack (= 7.2.2.2) - actiontext (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activemodel (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + rails (7.2.3) + actioncable (= 7.2.3) + actionmailbox (= 7.2.3) + actionmailer (= 7.2.3) + actionpack (= 7.2.3) + actiontext (= 7.2.3) + actionview (= 7.2.3) + activejob (= 7.2.3) + activemodel (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) bundler (>= 1.15.0) - railties (= 7.2.2.2) + railties (= 7.2.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -344,19 +351,21 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + railties (7.2.3) + actionpack (= 7.2.3) + activesupport (= 7.2.3) + cgi irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.0) + rake (13.3.1) ransack (4.3.0) activerecord (>= 6.1.5) activesupport (>= 6.1.5) @@ -366,15 +375,16 @@ GEM ffi (~> 1.0) rbs (3.9.5) logger - rdoc (6.14.2) + rdoc (7.2.0) erb psych (>= 4.0.0) + tsort redis (5.4.1) redis-client (>= 0.22.0) redis-client (0.26.0) connection_pool regexp_parser (2.10.0) - reline (0.6.2) + reline (0.6.3) io-console (~> 0.5) responders (3.1.1) actionpack (>= 5.2) @@ -490,11 +500,12 @@ GEM sprockets (>= 3.0.0) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.7) + stringio (3.2.0) syntax_tree (6.3.0) prettier_print (>= 1.2.0) - thor (1.4.0) - timeout (0.4.3) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) turbo-rails (2.0.16) actionpack (>= 7.1.0) railties (>= 7.1.0) @@ -522,7 +533,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.3) + zeitwerk (2.7.5) PLATFORMS aarch64-linux-gnu @@ -554,12 +565,12 @@ DEPENDENCIES jbuilder (~> 2.7) letter_opener_web listen (~> 3.3) - lsa_tdx_feedback (~> 1.0.1) + lsa_tdx_feedback (~> 1.0, >= 1.0.4) pg pry-byebug pry-rails puma (~> 6.0) - rails (~> 7.2.2, >= 7.2.2.1) + rails (~> 7.2.3, >= 7.2.3) rails-controller-testing rspec-rails rspec-sqlimit @@ -586,7 +597,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.4p34 + ruby 3.4.9 BUNDLED WITH - 2.7.0 + 4.0.8 diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 0000000..bb7b9f2 --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,30 @@ +class Users::SessionsController < Devise::SessionsController + def create + sanitize_null_bytes_in_sign_in_params! + super + end + + private + + def sanitize_null_bytes_in_sign_in_params! + auth_key = resource_name + auth_params = params[auth_key] + return unless auth_params.respond_to?(:to_unsafe_h) + + sanitized = sanitize_value(auth_params.to_unsafe_h) + params[auth_key] = ActionController::Parameters.new(sanitized) + end + + def sanitize_value(value) + case value + when String + value.delete("\u0000") + when Hash + value.transform_values { |v| sanitize_value(v) } + when Array + value.map { |v| sanitize_value(v) } + else + value + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 62b8283..2672847 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -8,4 +8,12 @@ def full_title(page_title = '') "#{page_title} | #{base_title}" end end + + def sentry_trace_propagation_meta + return ''.html_safe unless defined?(Sentry) + + Sentry.get_trace_propagation_meta.html_safe + rescue Encoding::CompatibilityError, ArgumentError + ''.html_safe + end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f971801..b32bdb7 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -16,7 +16,7 @@ <%= javascript_importmap_tags %> - <%= Sentry.get_trace_propagation_meta.html_safe %> + <%= sentry_trace_propagation_meta %> diff --git a/config/application.rb b/config/application.rb index c66bf5f..c4f8aed 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,4 +1,5 @@ require_relative 'boot' +require_relative '../lib/header_encoding_sanitizer_middleware' require 'rails' # Pick the frameworks you want: @@ -55,5 +56,8 @@ class Application < Rails::Application if defined?(DartSass) config.dartsass.load_paths << "#{Gem.loaded_specs['trix'].full_gem_path}/app/assets/stylesheets" end + + # Normalize malformed request header encodings before they reach app code. + config.middleware.use HeaderEncodingSanitizerMiddleware end end diff --git a/config/routes.rb b/config/routes.rb index 1447392..a618eb0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,7 @@ root to: 'static_pages#home' # Devise routes - devise_for :users + devise_for :users, controllers: { sessions: 'users/sessions' } devise_for :admin_users, ActiveAdmin::Devise.config # LSA TDX Feedback routes diff --git a/lib/header_encoding_sanitizer_middleware.rb b/lib/header_encoding_sanitizer_middleware.rb new file mode 100644 index 0000000..dd85871 --- /dev/null +++ b/lib/header_encoding_sanitizer_middleware.rb @@ -0,0 +1,38 @@ +class HeaderEncodingSanitizerMiddleware + HEADER_ENV_KEYS = /\AHTTP_/.freeze + EXTRA_HEADER_KEYS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze + + def initialize(app) + @app = app + end + + def call(env) + sanitize_request_headers!(env) + @app.call(env) + end + + private + + def sanitize_request_headers!(env) + env.each do |key, value| + next unless header_key?(key) + next unless value.is_a?(String) + + env[key] = sanitize_string(value) + end + end + + def header_key?(key) + key.match?(HEADER_ENV_KEYS) || EXTRA_HEADER_KEYS.include?(key) + end + + def sanitize_string(value) + return value if value.encoding == Encoding::UTF_8 && value.valid_encoding? + + value.dup.encode(Encoding::UTF_8, value.encoding, invalid: :replace, undef: :replace, replace: '') + rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError + value.dup.force_encoding(Encoding::UTF_8).scrub + rescue StandardError + '' + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5eb6e7c..d880857 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -10,4 +10,17 @@ expect(helper.full_title('Help')).to eq('Help | NELP Payments') end end + + describe '#sentry_trace_propagation_meta' do + it 'returns safe empty string when sentry metadata raises an argument error' do + stub_const('Sentry', Class.new do + def self.get_trace_propagation_meta + 'trace-meta' + end + end) + allow(Sentry).to receive(:get_trace_propagation_meta).and_raise(ArgumentError) + + expect(helper.sentry_trace_propagation_meta).to eq('') + end + end end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index 711189e..7c37be7 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -110,6 +110,16 @@ # Test scopes describe 'scopes' do + describe '.for_program_year' do + let!(:payment_2024) { create(:payment, program_year: 2024) } + let!(:payment_2025) { create(:payment, program_year: 2025) } + + it 'returns payments for the specified program year' do + expect(described_class.for_program_year(2024)).to contain_exactly(payment_2024) + expect(described_class.for_program_year(2025)).to contain_exactly(payment_2025) + end + end + describe '.current_program_payments' do let!(:current_program) { create(:program_setting, :active, program_year: 2024) } let!(:old_program) { create(:program_setting, program_year: 2023) } diff --git a/spec/requests/admin_dashboard_request_spec.rb b/spec/requests/admin_dashboard_request_spec.rb new file mode 100644 index 0000000..3c02288 --- /dev/null +++ b/spec/requests/admin_dashboard_request_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +RSpec.describe 'Admin dashboard', type: :request do + let(:admin_user) do + create(:admin_user, email: 'admin-dashboard@example.com', password: 'password', password_confirmation: 'password') + end + + before do + sign_in admin_user + end + + it 'renders dashboard when no active program exists' do + get '/admin' + + expect(response).to have_http_status(:success) + expect(response.body).to include('No active program found') + end + + it 'renders payment sections when an active program exists' do + program = create(:program_setting, :active, program_year: 2024, program_fee: 1000, application_fee: 500) + user = create(:user, email: 'payer@example.com') + create(:payment, user: user, program_year: program.program_year, total_amount: '50000', transaction_status: '1') + + get '/admin' + + expect(response).to have_http_status(:success) + expect(response.body).to include('User Payment Totals - Program Year 2024') + expect(response.body).to include('Recent Payments - Program Year 2024') + expect(response.body).to include('payer@example.com') + end + + it 'renders empty payment states for active program without payments' do + create(:program_setting, :active, program_year: 2024, program_fee: 1000, application_fee: 500) + + get '/admin' + + expect(response).to have_http_status(:success) + expect(response.body).to include('No successful payments found for the active program.') + expect(response.body).to include('No payments found for the active program.') + end + + it 'supports dashboard sorting and pagination paths' do + program = create(:program_setting, :active, program_year: 2024, program_fee: 1000, application_fee: 500) + create_list(:user, 21).each_with_index do |user, idx| + create(:payment, + user: user, + program_year: program.program_year, + total_amount: (50_000 + (idx * 1000)).to_s, + transaction_status: '1') + end + + get '/admin', params: { sort_column: 'user', sort_order: 'asc', page: 2 } + expect(response).to have_http_status(:success) + expect(response.body).to include('User Payment Totals - Program Year 2024') + expect(response.body).to include('Previous') + + get '/admin', params: { sort_column: 'balance_due', sort_order: 'asc', page: 1 } + expect(response).to have_http_status(:success) + expect(response.body).to include('Next') + end + + it 'renders admin users index and form' do + get '/admin/admin_users' + expect(response).to have_http_status(:success) + + get '/admin/admin_users/new' + expect(response).to have_http_status(:success) + expect(response.body).to include('Password confirmation') + end + + it 'renders static pages index' do + create(:static_page, location: 'terms') + + get '/admin/static_pages' + + expect(response).to have_http_status(:success) + expect(response.body).to include('Manage messages on static pages') + end + + it 'renders static page edit form with rich text input' do + static_page = create(:static_page, location: 'privacy') + + get "/admin/static_pages/#{static_page.id}/edit" + + expect(response).to have_http_status(:success) + expect(response.body).to include('trix') + end +end diff --git a/spec/requests/static_pages_request_spec.rb b/spec/requests/static_pages_request_spec.rb new file mode 100644 index 0000000..c95a971 --- /dev/null +++ b/spec/requests/static_pages_request_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe 'StaticPages', type: :request do + describe 'GET /terms with malformed headers' do + it 'responds successfully when HTTP_USER_AGENT contains invalid bytes' do + malformed_user_agent = "12345'\"\\');|]*%00{%0d%0a<%00>\xBF\x27".b + + get terms_path, headers: { 'HTTP_USER_AGENT' => malformed_user_agent } + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/requests/user_sessions_request_spec.rb b/spec/requests/user_sessions_request_spec.rb new file mode 100644 index 0000000..89e7957 --- /dev/null +++ b/spec/requests/user_sessions_request_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +RSpec.describe 'UserSessions', type: :request do + describe 'POST /users/sign_in' do + let(:password) { 'Password123!' } + let!(:user) { create(:user, email: 'sanitize@example.com', password: password, password_confirmation: password) } + + it 'authenticates successfully when email contains a null byte' do + post user_session_path, params: { + user: { + email: "sanitize@example.com\u0000", + password: password, + }, + } + + expect(response).to redirect_to(root_path) + expect(request.env['warden'].user).to eq(user) + end + + it 'authenticates with extra nested params containing null bytes' do + post user_session_path, params: { + user: { + email: "sanitize@example.com\u0000", + password: password, + metadata: { + token: "abc\u0000123", + flags: ["a\u0000", "b"], + }, + }, + } + + expect(response).to redirect_to(root_path) + expect(request.env['warden'].user).to eq(user) + end + + it 'authenticates when nested metadata includes non-string values' do + post user_session_path, params: { + user: { + email: "sanitize@example.com\u0000", + password: password, + metadata: { + attempts: 2, + token: "xyz\u0000", + }, + }, + } + + expect(response).to redirect_to(root_path) + expect(request.env['warden'].user).to eq(user) + end + + it 'does not raise when user params are missing' do + expect do + post user_session_path, params: {} + end.not_to raise_error + + expect(response).to have_http_status(:ok) + expect(response.body).to include('Log in') + end + end +end