Extract OidcUserConsent.record! upsert for shared consent

The consent record is unique on user+application and shared between the browser
authorization flow and device-flow approval. Centralize the upsert in
OidcUserConsent.record!(merge:) so a narrower device request unions its scopes
into any existing grant (merge: true) rather than overwriting it and wiping
stored claims, while the browser consent screen records exactly the approved
scopes (merge: false). scopes is now nil-safe for not-yet-saved records.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
This commit is contained in:
Dan Milne
2026-07-19 13:59:44 +10:00
co-authored by Claude Opus 4.8
parent 7eddea8356
commit e3f0bd4cab
3 changed files with 70 additions and 11 deletions
+39
View File
@@ -206,6 +206,45 @@ class OidcUserConsentTest < ActiveSupport::TestCase
assert consent.covers_scopes?(["openid", "email", "profile"])
end
# --- .record! upsert semantics --------------------------------------------
test "record! creates a new consent with the given scopes and claims" do
user = users(:alice)
app = applications(:another_app)
OidcUserConsent.where(user: user, application: app).delete_all
consent = OidcUserConsent.record!(user: user, application: app,
scopes: %w[openid email], claims_requests: {"userinfo" => {"email" => nil}})
assert_equal %w[openid email], consent.scopes
assert_equal({"userinfo" => {"email" => nil}}, consent.parsed_claims_requests)
end
test "record! without merge overwrites scopes (browser flow)" do
user = users(:alice)
app = applications(:another_app)
OidcUserConsent.create!(user: user, application: app,
scopes_granted: "openid email profile", granted_at: 1.day.ago)
consent = OidcUserConsent.record!(user: user, application: app, scopes: %w[openid],
claims_requests: {})
assert_equal %w[openid], consent.scopes, "browser flow records exactly what was consented"
end
test "record! with merge unions scopes and preserves stored claims (device flow)" do
user = users(:alice)
app = applications(:another_app)
OidcUserConsent.create!(user: user, application: app,
scopes_granted: "openid email profile", claims_requests: {"userinfo" => {"email" => nil}},
granted_at: 1.day.ago)
consent = OidcUserConsent.record!(user: user, application: app, scopes: %w[openid], merge: true)
assert_equal %w[openid email profile].sort, consent.scopes.sort, "merge must not shrink a prior grant"
assert_equal({"userinfo" => {"email" => nil}}, consent.parsed_claims_requests, "merge must not wipe claims")
end
test "should validate scope coverage logic with real OIDC scenarios" do
# Typical OIDC consent scenario
@consent.scopes_granted = "openid profile email"