require "test_helper" class OidcAuthorizationCodeTest < ActiveSupport::TestCase def setup @auth_code = oidc_authorization_codes(:one) end test "should be valid with all required attributes" do assert @auth_code.valid? end test "should belong to an application" do assert_respond_to @auth_code, :application assert_equal applications(:kavita_app), @auth_code.application end test "should belong to a user" do assert_respond_to @auth_code, :user assert_equal users(:alice), @auth_code.user end test "should generate code before validation on create" do new_code = OidcAuthorizationCode.new( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback" ) assert_nil new_code.code assert new_code.save assert_not_nil new_code.code assert_match /^[A-Za-z0-9_-]+$/, new_code.code end test "should set expiry before validation on create" do new_code = OidcAuthorizationCode.new( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback" ) assert_nil new_code.expires_at assert new_code.save assert_not_nil new_code.expires_at assert new_code.expires_at > Time.current assert new_code.expires_at <= 11.minutes.from_now # Allow some variance end test "should validate presence of code" do @auth_code.code = nil assert_not @auth_code.valid? assert_includes @auth_code.errors[:code], "can't be blank" end test "should validate uniqueness of code" do @auth_code.save! if @auth_code.changed? duplicate = OidcAuthorizationCode.new( code: @auth_code.code, application: applications(:another_app), user: users(:bob), redirect_uri: "https://example.com/callback" ) assert_not duplicate.valid? assert_includes duplicate.errors[:code], "has already been taken" end test "should validate presence of redirect_uri" do @auth_code.redirect_uri = nil assert_not @auth_code.valid? assert_includes @auth_code.errors[:redirect_uri], "can't be blank" end test "should identify expired codes correctly" do @auth_code.expires_at = 5.minutes.ago assert @auth_code.expired?, "Should identify past expiry as expired" @auth_code.expires_at = 5.minutes.from_now assert_not @auth_code.expired?, "Should identify future expiry as not expired" @auth_code.expires_at = Time.current assert @auth_code.expired?, "Should identify current time as expired" end test "should identify usable codes correctly" do # Fresh, unused code should be usable @auth_code.expires_at = 5.minutes.from_now @auth_code.used = false assert @auth_code.usable?, "Fresh unused code should be usable" # Used code should not be usable @auth_code.used = true assert_not @auth_code.usable?, "Used code should not be usable" # Expired code should not be usable @auth_code.used = false @auth_code.expires_at = 5.minutes.ago assert_not @auth_code.usable?, "Expired code should not be usable" # Used and expired code should not be usable @auth_code.used = true @auth_code.expires_at = 5.minutes.ago assert_not @auth_code.usable?, "Used and expired code should not be usable" end test "should consume code correctly" do @auth_code.used = false assert_not @auth_code.used?, "Code should initially be unused" @auth_code.consume! @auth_code.reload assert @auth_code.used?, "Code should be marked as used after consumption" end test "valid scope should return only unused and non-expired codes" do # Create codes with different states valid_code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback" ) used_code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback", used: true ) expired_code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback", expires_at: 5.minutes.ago ) valid_codes = OidcAuthorizationCode.valid assert_includes valid_codes, valid_code assert_not_includes valid_codes, used_code assert_not_includes valid_codes, expired_code end test "expired scope should return only expired codes" do # Create codes with different expiry states non_expired_code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback", expires_at: 5.minutes.from_now ) expired_code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback", expires_at: 5.minutes.ago ) expired_codes = OidcAuthorizationCode.expired assert_includes expired_codes, expired_code assert_not_includes expired_codes, non_expired_code end test "should handle concurrent consumption safely" do @auth_code.used = false @auth_code.save! # Simulate concurrent consumption original_used = @auth_code.used? @auth_code.consume! assert_not original_used, "Code should be unused before consumption" assert @auth_code.used?, "Code should be used after consumption" end test "should generate secure random codes" do codes = [] 5.times do code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback" ) codes << code.code end # All codes should be unique assert_equal codes.length, codes.uniq.length # All codes should match the expected pattern codes.each do |code| assert_match /^[A-Za-z0-9_-]+$/, code assert_equal 43, code.length # Base64 padding removed end end end