diff --git a/app/models/user.rb b/app/models/user.rb index 32093a87b..21351d6fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -83,6 +83,14 @@ class User < ApplicationRecord invalid_editor_version: "%s must be either v1 or v2", reserved_username: "username is reserved" }.freeze + # follow the syntax in https://interledger.org/rfcs/0026-payment-pointers/#payment-pointer-syntax + PAYMENT_POINTER_REGEXP = %r{ + \A # start + \$ # starts with a dollar sign + ([a-zA-Z0-9\-.])+ # matches the hostname (ex ilp.uphold.com) + (/[\x20-\x7F]+)? # optional forward slash and identifier with printable ASCII characters + \z + }x.freeze attr_accessor :scholar_email, :new_note, :note_for_current_role, :user_status, :pro, :merge_user_id, :add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ip_address @@ -205,6 +213,7 @@ class User < ApplicationRecord validates :inbox_type, inclusion: { in: INBOXES } validates :name, length: { in: 1..100 } validates :password, length: { in: 8..100 }, allow_nil: true + validates :payment_pointer, format: PAYMENT_POINTER_REGEXP, allow_nil: true validates :rating_votes_count, presence: true validates :reactions_count, presence: true validates :sign_in_count, presence: true @@ -252,6 +261,7 @@ class User < ApplicationRecord # make sure usernames are not empty, to be able to use the database unique index before_validation :verify_email before_validation :set_username + before_validation :strip_payment_pointer before_create :set_default_language before_destroy :unsubscribe_from_newsletters, prepend: true before_destroy :destroy_follows, prepend: true @@ -669,4 +679,8 @@ class User < ApplicationRecord errors.add(:password, "doesn't match password confirmation") end + + def strip_payment_pointer + self.payment_pointer = payment_pointer.strip if payment_pointer + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 367458c9f..1f960cb76 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -187,6 +187,11 @@ RSpec.describe User, type: :model do it { is_expected.not_to allow_value("AcMe_1%").for(:username) } it { is_expected.to allow_value("AcMe_1").for(:username) } + it { is_expected.not_to allow_value("$example.com/value\x1F").for(:payment_pointer) } + it { is_expected.not_to allow_value("example.com/value").for(:payment_pointer) } + it { is_expected.to allow_value(" $example.com/value ").for(:payment_pointer) } + it { is_expected.to allow_value(nil).for(:payment_pointer) } + it { is_expected.to validate_inclusion_of(:inbox_type).in_array(%w[open private]) } it { is_expected.to validate_length_of(:email).is_at_most(50).allow_nil } diff --git a/spec/system/user/user_edits_extensions_spec.rb b/spec/system/user/user_edits_extensions_spec.rb index 72af98f94..7d3893947 100644 --- a/spec/system/user/user_edits_extensions_spec.rb +++ b/spec/system/user/user_edits_extensions_spec.rb @@ -53,4 +53,17 @@ RSpec.describe "User edits their extensions", type: :system, js: true do expect(page).to have_text("Feed url is not a valid RSS/Atom feed") end end + + describe "PaymentPointer" do + before do + visit user_settings_path(:extensions) + end + + it "fails if the payment pointer is invalid" do + fill_in "user[payment_pointer]", with: "invalid_example/value" + click_on "Save Web Monetization Settings" + + expect(page).to have_text("Payment pointer is invalid") + end + end end