require "test_helper" class OidcAccessTokenTest < ActiveSupport::TestCase def setup @access_token = oidc_access_tokens(:one) end test "should be valid with all required attributes" do assert @access_token.valid? end test "should belong to an application" do assert_respond_to @access_token, :application assert_equal applications(:kavita_app), @access_token.application end test "should belong to a user" do assert_respond_to @access_token, :user assert_equal users(:alice), @access_token.user end test "should generate token before validation on create" do new_token = OidcAccessToken.new( application: applications(:kavita_app), user: users(:alice) ) assert_nil new_token.token assert new_token.save assert_not_nil new_token.token assert_match /^[A-Za-z0-9_-]+$/, new_token.token end test "should set expiry before validation on create" do new_token = OidcAccessToken.new( application: applications(:kavita_app), user: users(:alice) ) assert_nil new_token.expires_at assert new_token.save assert_not_nil new_token.expires_at assert new_token.expires_at > Time.current assert new_token.expires_at <= 61.minutes.from_now # Allow some variance end test "should validate presence of token" do @access_token.token = nil assert_not @access_token.valid? assert_includes @access_token.errors[:token], "can't be blank" end test "should validate uniqueness of token" do @access_token.save! if @access_token.changed? duplicate = OidcAccessToken.new( token: @access_token.token, application: applications(:another_app), user: users(:bob) ) assert_not duplicate.valid? assert_includes duplicate.errors[:token], "has already been taken" end test "should identify expired tokens correctly" do @access_token.expires_at = 5.minutes.ago assert @access_token.expired?, "Should identify past expiry as expired" @access_token.expires_at = 5.minutes.from_now assert_not @access_token.expired?, "Should identify future expiry as not expired" @access_token.expires_at = Time.current assert @access_token.expired?, "Should identify current time as expired" end test "should identify active tokens correctly" do # Non-expired token should be active @access_token.expires_at = 5.minutes.from_now assert @access_token.active?, "Future expiry should be active" # Expired token should not be active @access_token.expires_at = 5.minutes.ago assert_not @access_token.active?, "Past expiry should not be active" # Current time should be considered expired (not active) @access_token.expires_at = Time.current assert_not @access_token.active?, "Current time should not be active" end test "should revoke token correctly" do @access_token.expires_at = 1.hour.from_now original_expiry = @access_token.expires_at assert @access_token.active?, "Token should be active before revocation" @access_token.revoke! @access_token.reload assert @access_token.expired?, "Token should be expired after revocation" assert @access_token.expires_at <= Time.current, "Expiry should be set to current time or earlier" assert @access_token.expires_at < original_expiry, "Expiry should be changed from original" end test "valid scope should return only non-expired tokens" do # Create tokens with different states valid_token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice) ) expired_token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice), expires_at: 5.minutes.ago ) valid_tokens = OidcAccessToken.valid assert_includes valid_tokens, valid_token assert_not_includes valid_tokens, expired_token end test "expired scope should return only expired tokens" do # Create tokens with different expiry states non_expired_token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice), expires_at: 1.hour.from_now ) expired_token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice), expires_at: 5.minutes.ago ) expired_tokens = OidcAccessToken.expired assert_includes expired_tokens, expired_token assert_not_includes expired_tokens, non_expired_token end test "should handle concurrent revocation safely" do @access_token.expires_at = 1.hour.from_now @access_token.save! original_active = @access_token.active? @access_token.revoke! assert original_active, "Token should be active before revocation" assert @access_token.expired?, "Token should be expired after revocation" end test "should generate secure random tokens" do tokens = [] 5.times do token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice) ) tokens << token.token end # All tokens should be unique assert_equal tokens.length, tokens.uniq.length # All tokens should match the expected pattern tokens.each do |token| assert_match /^[A-Za-z0-9_-]+$/, token assert_equal 63, token.length # Base64 with padding removed (48 bytes = 64 chars, minus padding) end end test "should have longer token than authorization codes" do auth_code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback" ) access_token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice) ) assert access_token.token.length > auth_code.code.length, "Access tokens should be longer than authorization codes" end test "should have appropriate expiry times" do auth_code = OidcAuthorizationCode.create!( application: applications(:kavita_app), user: users(:alice), redirect_uri: "https://example.com/callback" ) access_token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice) ) # Authorization codes expire in 10 minutes, access tokens in 1 hour assert access_token.expires_at > auth_code.expires_at, "Access tokens should have longer expiry than authorization codes" end test "revoked tokens should not appear in valid scope" do access_token = OidcAccessToken.create!( application: applications(:kavita_app), user: users(:alice) ) # Token should be in valid scope initially assert_includes OidcAccessToken.valid, access_token # Revoke the token access_token.revoke! # Token should no longer be in valid scope assert_not_includes OidcAccessToken.valid, access_token end end