Files
clinch/test/models/oidc_device_code_test.rb
T
Dan MilneandClaude Opus 4.8 8b146db54e Device code: retry user_code generation on collision
Regenerate the user_code when a freshly generated one collides with an existing
code (up to USER_CODE_MAX_ATTEMPTS), so a client never gets a uniqueness error
just because two codes happened to match; the DB unique index stays the final
guard against a concurrent-insert race.

Replace the model test that asserted Rails' uniqueness validator message
("has already been taken") — per CLAUDE.md we don't test framework behavior —
with one that exercises the app-specific property: generation retries past a
collision and yields a unique code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
2026-07-19 14:43:03 +10:00

93 lines
3.5 KiB
Ruby

require "test_helper"
class OidcDeviceCodeTest < ActiveSupport::TestCase
def setup
@application = Application.create!(
name: "Device Code Model Test",
slug: "device-code-model-test",
app_type: "oidc",
is_public_client: true,
active: true
)
@user = User.create!(email_address: "device_model@example.com", password: "password123")
end
test "generates an opaque device_code stored as HMAC and looked up by plaintext" do
dc = OidcDeviceCode.create!(application: @application)
assert dc.plaintext_device_code.present?
assert dc.device_code_hmac.present?
assert_not_equal dc.plaintext_device_code, dc.device_code_hmac
assert_equal dc, OidcDeviceCode.find_by_plaintext_device_code(dc.plaintext_device_code)
assert_nil OidcDeviceCode.find_by_plaintext_device_code("wrong")
end
test "generates a short user_code from the unambiguous alphabet" do
dc = OidcDeviceCode.create!(application: @application)
assert_equal 8, dc.user_code.length
# No visually ambiguous characters (0/O, 1/I) and only the allowed alphabet.
assert_match(/\A[A-HJ-NP-Z2-9]{8}\z/, dc.user_code)
end
test "find_by_user_code normalizes case, hyphens, and whitespace" do
dc = OidcDeviceCode.create!(application: @application)
formatted = "#{dc.user_code[0, 4]}-#{dc.user_code[4, 4]}".downcase
assert_equal dc, OidcDeviceCode.find_by_user_code(formatted)
assert_equal dc, OidcDeviceCode.find_by_user_code(" #{dc.user_code} ")
assert_nil OidcDeviceCode.find_by_user_code("nope")
end
test "regenerates the user_code when generation collides with an existing code" do
existing = OidcDeviceCode.create!(application: @application)
taken = existing.user_code
fresh = "ABCDEFGH" # in-alphabet, effectively guaranteed != the random `taken`
# First candidate collides with the existing code, the second is unique — the
# generator must retry rather than surface a uniqueness error.
candidates = [taken, fresh].each
dc = OidcDeviceCode.new(application: @application)
dc.define_singleton_method(:random_user_code) { candidates.next }
dc.save!
assert_equal fresh, dc.user_code
assert_not_equal taken, dc.user_code
end
test "starts pending and approve! attaches the user and auth context" do
dc = OidcDeviceCode.create!(application: @application)
assert dc.pending?
dc.approve!(user: @user, acr: "1", auth_time: 1_700_000_000)
assert dc.approved?
assert_equal @user, dc.user
assert_equal "1", dc.acr
assert_equal 1_700_000_000, dc.auth_time
end
test "deny! marks the code denied" do
dc = OidcDeviceCode.create!(application: @application)
dc.deny!
assert dc.denied?
end
test "expired? reflects expires_at" do
assert OidcDeviceCode.create!(application: @application, expires_at: 1.minute.ago).expired?
assert_not OidcDeviceCode.create!(application: @application).expired?
end
test "uses_pkce? and rejects malformed code_challenge" do
assert_not OidcDeviceCode.create!(application: @application).uses_pkce?
valid_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
dc = OidcDeviceCode.create!(application: @application, code_challenge: valid_challenge, code_challenge_method: "S256")
assert dc.uses_pkce?
bad = OidcDeviceCode.new(application: @application, code_challenge: "too-short")
assert_not bad.valid?
assert_includes bad.errors[:code_challenge], "must be 43-128 characters of base64url encoding"
end
end