chore(deps): upgrade to Rails 7.2.3.2, Ruby 3.4.10 and latest gems

Upgrades Rails from 7.1.6 to 7.2.3.2, Ruby from 3.4.6 to 3.4.10, and
brings the rest of the bundle up to date.

Two things were broken by Rails 7.2 and are fixed here:

* MigrationWaiter called `ActiveRecord::Base.connection.migration_context`,
  which moved to the connection pool in 7.2. This raised NoMethodError on
  boot for any deployment with `migration_waiter.enabled` set, and had no
  test covering it.
* `config.action_dispatch.show_exceptions = false` was deprecated in 7.1
  and removed in 7.2. Now `:none`.

`json` is pinned to < 3. Active Support 7.2 passes `quirks_mode:` to both
`JSON.generate` and `JSON.parse`, and json 3.0 removed that option, so an
unpinned json resolves to 3.x and makes every `to_json` raise - webhook
payloads included. Nothing else in the bundle constrains it. The pin can go
when we reach Rails 8.1, which no longer passes the option.

`dynamic_form` is dropped. It is unmaintained and from 1.3.1 requires Ruby
< 3.4, so it could never be updated again. Ten views used it, so
`error_messages_for` now lives in ErrorMessagesHelper and `f.error_messages`
in a form builder extension. The rendered markup is unchanged apart from a
heading and description the stylesheet had always hidden; the rules that
hid them are removed too.

`execjs` is unpinned. The `< 2.8` constraint dated from the Rails 6.0
upgrade and asset output is byte-identical without it.

Notable gem majors: puma 8, haml 7, secure_headers 7, sentry 7, jwt 3,
prometheus-client 5, highline 3, omniauth-rails_csrf_protection 2,
rspec-rails 8, shoulda-matchers 8, rubocop 1.91. secure_headers 7 changes
X-XSS-Protection from "1; mode=block" to "0", which is the correct modern
default and matches Rails' own.

`config.load_defaults` deliberately stays at 7.0. The pending 7.1 and 7.2
options are listed, commented out, in new_framework_defaults_7_1.rb and
_7_2.rb so they can be adopted in stages - several change the signed cookie
and cache formats and need a staged deploy. The vestigial
new_framework_defaults_7_0.rb is removed, as `load_defaults 7.0` already
applies all of it.

Adds tests for the areas this touched, which were mostly uncovered:
MigrationWaiter, the serialized columns that gate a defaults bump, JSON
encoding, HAML template compilation for all 103 templates, page rendering,
form error rendering and security headers. The assets Gemfile group now
includes :test so the view layer can be exercised at all.
This commit is contained in:
Adam Cooke
2026-09-15 10:27:16 +01:00
parent ced77a3860
commit 5eeed7c023
23 changed files with 1010 additions and 361 deletions
+68
View File
@@ -0,0 +1,68 @@
# frozen_string_literal: true
require "rails_helper"
# Active Record's serialization defaults have changed across recent Rails
# releases (`config.active_record.default_column_serializer` becomes `nil` in
# the 7.1 defaults, which makes an implicit coder an error rather than YAML).
# These columns hold data written by older versions of Postal, so the on-disk
# format has to stay YAML. These examples pin both the format and the round trip
# so a future framework defaults bump cannot change them silently.
RSpec.describe "serialized columns" do
shared_examples "a YAML serialized hash column" do |attribute|
let(:value) { { "string" => "value", "number" => 123, "nested" => { "a" => [1, 2] } } }
def write_raw(record, attribute, raw)
sql = described_class.sanitize_sql_array(
["UPDATE #{described_class.table_name} SET #{attribute} = ? WHERE id = ?", raw, record.id]
)
described_class.connection.update(sql)
end
def read_raw(record, attribute)
described_class.connection.select_value(
described_class.sanitize_sql_array(
["SELECT #{attribute} FROM #{described_class.table_name} WHERE id = ?", record.id]
)
)
end
it "defaults to an empty hash" do
expect(described_class.new.public_send(attribute)).to eq({})
end
it "round trips a hash through the database" do
record.update!(attribute => value)
expect(record.reload.public_send(attribute)).to eq value
end
it "stores the value as YAML in the database" do
record.update!(attribute => value)
raw = read_raw(record, attribute)
expect(raw).to be_a String
expect(YAML.safe_load(raw)).to eq value
end
it "reads YAML which was written by a previous version of Postal" do
write_raw(record, attribute, value.to_yaml)
expect(record.reload.public_send(attribute)).to eq value
end
it "rejects a value which is not a hash" do
expect { record.update!(attribute => "not a hash") }.to raise_error ActiveRecord::SerializationTypeMismatch
end
end
describe Credential do
let(:record) { create(:credential) }
include_examples "a YAML serialized hash column", :options
end
describe WebhookRequest do
let(:record) { create(:webhook_request) }
include_examples "a YAML serialized hash column", :payload
end
end