[deploy] Improve some validations (#7335)

* Validations

* Fix specs

* Revert twitter tag change
This commit is contained in:
rhymes 2020-04-16 18:59:12 +02:00 committed by GitHub
parent 8f6fb35404
commit 0a6be8b967
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 136 deletions

View file

@ -1,59 +1,61 @@
class Organization < ApplicationRecord
include CloudinaryHelper
COLOR_HEX_REGEXP = /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/.freeze
INTEGER_REGEXP = /\A\d+\z/.freeze
SLUG_REGEXP = /\A[a-zA-Z0-9\-_]+\z/.freeze
MESSAGES = {
integer_only: "Integer only. No sign allowed.",
reserved_word: "%<value>s is a reserved word. Contact site admins for help registering your organization."
}.freeze
acts_as_followable
has_many :job_listings
has_many :organization_memberships, dependent: :delete_all
has_many :users, through: :organization_memberships
has_many :api_secrets, through: :users
has_many :articles
has_many :classified_listings
has_many :collections
has_many :credits
has_many :display_ads
has_many :notifications
has_many :credits
has_many :unspent_credits, -> { where spent: false }, class_name: "Credit", inverse_of: :organization
has_many :classified_listings
has_many :organization_memberships, dependent: :delete_all
has_many :profile_pins, as: :profile, inverse_of: :profile
has_many :sponsorships
has_many :unspent_credits, -> { where spent: false }, class_name: "Credit", inverse_of: :organization
has_many :users, through: :organization_memberships
validates :name, :summary, :url, :profile_image, presence: true
validates :name,
length: { maximum: 50 }
validates :summary,
length: { maximum: 250 }
validates :tag_line,
length: { maximum: 60 }
validates :text_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
validates :bg_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
validates :slug,
presence: true,
uniqueness: { case_sensitive: false },
format: { with: /\A[a-zA-Z0-9\-_]+\Z/ },
length: { in: 2..18 },
exclusion: { in: ReservedWords.all,
message: "%<value>s is a reserved word. Contact site admins for help registering your organization." }
validates :url, url: { allow_blank: true, no_local: true, schemes: %w[https http] }
validates :secret, uniqueness: { allow_blank: true }
validates :location, :email, :company_size, length: { maximum: 64 }
validates :company_size, format: { with: /\A\d+\z/,
message: "Integer only. No sign allowed.",
allow_blank: true }
validates :tech_stack, :story, length: { maximum: 640 }
validates :cta_button_url,
url: { allow_blank: true, no_local: true, schemes: %w[https http] }
validates :cta_button_text, length: { maximum: 20 }
validates :bg_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
validates :company_size, format: { with: INTEGER_REGEXP, message: MESSAGES[:integer_only], allow_blank: true }
validates :company_size, length: { maximum: 7 }, allow_nil: true
validates :cta_body_markdown, length: { maximum: 256 }
before_save :remove_at_from_usernames
after_save :bust_cache
before_save :generate_secret
before_save :update_articles
before_validation :downcase_slug
before_validation :check_for_slug_change
before_validation :evaluate_markdown
validates :cta_button_text, length: { maximum: 20 }
validates :cta_button_url, length: { maximum: 150 }, url: { allow_blank: true, no_local: true }
validates :github_username, length: { maximum: 50 }
validates :location, :email, length: { maximum: 64 }
validates :name, :summary, :url, :profile_image, presence: true
validates :name, length: { maximum: 50 }
validates :proof, length: { maximum: 1500 }
validates :secret, length: { is: 100 }, uniqueness: { allow_blank: true }, allow_blank: true
validates :slug, exclusion: { in: ReservedWords.all, message: MESSAGES[:reserved_word] }
validates :slug, format: { with: SLUG_REGEXP }, length: { in: 2..18 }
validates :slug, presence: true, uniqueness: { case_sensitive: false }
validates :summary, length: { maximum: 250 }
validates :tag_line, length: { maximum: 60 }
validates :tech_stack, :story, length: { maximum: 640 }
validates :text_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
validates :twitter_username, length: { maximum: 15 }
validates :url, length: { maximum: 200 }, url: { allow_blank: true, no_local: true }
validate :unique_slug_including_users_and_podcasts, if: :slug_changed?
after_save :bust_cache
before_save :generate_secret
before_save :remove_at_from_usernames
before_save :update_articles
before_validation :check_for_slug_change
before_validation :downcase_slug
before_validation :evaluate_markdown
after_commit :sync_related_elasticsearch_docs, on: %i[update destroy]
mount_uploader :profile_image, ProfileImageUploader
@ -117,7 +119,7 @@ class Organization < ApplicationRecord
end
def downcase_slug
self.slug = slug.downcase
self.slug = slug&.downcase
end
def update_articles
@ -138,7 +140,14 @@ class Organization < ApplicationRecord
end
def unique_slug_including_users_and_podcasts
errors.add(:slug, "is taken.") if User.find_by(username: slug) || Podcast.find_by(slug: slug) || Page.find_by(slug: slug) || slug.include?("sitemap-")
slug_taken = (
User.exists?(username: slug) ||
Podcast.exists?(slug: slug) ||
Page.exists?(slug: slug) ||
slug&.include?("sitemap-")
)
errors.add(:slug, "is taken.") if slug_taken
end
def sync_related_elasticsearch_docs

View file

@ -1,5 +1,33 @@
class User < ApplicationRecord
include AlgoliaSearch
include CloudinaryHelper
include Searchable
include Storext.model
BEHANCE_URL_REGEXP = /\A(http(s)?:\/\/)?(www.behance.net|behance.net)\/.*\z/.freeze
COLOR_HEX_REGEXP = /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/.freeze
DRIBBBLE_URL_REGEXP = /\A(http(s)?:\/\/)?(www.dribbble.com|dribbble.com)\/.*\z/.freeze
EDITORS = %w[v1 v2].freeze
FACEBOOK_URL_REGEXP = /\A(http(s)?:\/\/)?(www.facebook.com|facebook.com)\/.*\z/.freeze
FONTS = %w[default sans_serif monospace comic_sans open_dyslexic].freeze
GITLAB_URL_REGEXP = /\A(http(s)?:\/\/)?(www.gitlab.com|gitlab.com)\/.*\z/.freeze
INBOXES = %w[open private].freeze
INSTAGRAM_URL_REGEXP = /\A(http(s)?:\/\/)?(?:www.)?instagram.com\/(?=.{1,30}\/?$)([a-zA-Z\d_]\.?)*[a-zA-Z\d_]+\/?\z/.freeze
LINKEDIN_URL_REGEXP = /\A(http(s)?:\/\/)?(www.linkedin.com|linkedin.com|[A-Za-z]{2}.linkedin.com)\/.*\z/.freeze
MEDIUM_URL_REGEXP = /\A(http(s)?:\/\/)?(www.medium.com|medium.com)\/.*\z/.freeze
NAVBARS = %w[default static].freeze
STACKOVERFLOW_URL_REGEXP = /\A(http(s)?:\/\/)?(((www|pt|ru|es|ja).)?stackoverflow.com|(www.)?stackexchange.com)\/.*\z/.freeze
STREAMING_PLATFORMS = %w[twitch].freeze
THEMES = %w[default night_theme pink_theme minimal_light_theme ten_x_hacker_theme].freeze
TWITCH_URL_REGEXP = /\A(http(s)?:\/\/)?(www.twitch.tv|twitch.tv)\/.*\z/.freeze
USERNAME_REGEXP = /\A[a-zA-Z0-9_]+\z/.freeze
MESSAGES = {
invalid_config_font: "%<value>s is not a valid font selection",
invalid_config_navbar: "%<value>s is not a valid navbar value",
invalid_config_theme: "%<value>s is not a valid theme",
invalid_editor_version: "%<value>s must be either v1 or v2",
reserved_username: "username is reserved"
}.freeze
attr_accessor(
:scholar_email, :new_note, :note_for_current_role, :user_status, :pro, :merge_user_id,
@ -8,10 +36,6 @@ class User < ApplicationRecord
rolify after_add: :index_roles, after_remove: :index_roles
include AlgoliaSearch
include Storext.model
include Searchable
SEARCH_SERIALIZER = Search::UserSerializer
SEARCH_CLASS = Search::User
DATA_SYNC_CLASS = DataSync::Elasticsearch::User
@ -70,82 +94,48 @@ class User < ApplicationRecord
devise :omniauthable, :registerable, :database_authenticatable, :confirmable, :rememberable
validates :email,
length: { maximum: 50 },
email: true,
allow_nil: true
validates :behance_url, length: { maximum: 100 }, allow_blank: true, format: BEHANCE_URL_REGEXP
validates :bg_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
validates :config_font, inclusion: { in: FONTS, message: MESSAGES[:invalid_config_font] }
validates :config_navbar, inclusion: { in: NAVBARS, message: MESSAGES[:invalid_config_navbar] }
validates :config_theme, inclusion: { in: THEMES, message: MESSAGES[:invalid_config_theme] }
validates :currently_streaming_on, inclusion: { in: STREAMING_PLATFORMS }, allow_nil: true
validates :dribbble_url, length: { maximum: 100 }, allow_blank: true, format: DRIBBBLE_URL_REGEXP
validates :editor_version, inclusion: { in: EDITORS, message: MESSAGES[:invalid_editor_version] }
validates :email, length: { maximum: 50 }, email: true, allow_nil: true
validates :email, uniqueness: { allow_nil: true, case_sensitive: false }, if: :email_changed?
validates :name, length: { minimum: 1, maximum: 100 }
validates :username,
presence: true,
format: { with: /\A[a-zA-Z0-9_]+\Z/ },
length: { in: 2..30 },
exclusion: { in: ReservedWords.all, message: "username is reserved" }
validates :username, uniqueness: { case_sensitive: false }, if: :username_changed?
validates :twitter_username, uniqueness: { allow_nil: true }, if: :twitter_username_changed?
validates :github_username, uniqueness: { allow_nil: true }, if: :github_username_changed?
validates :employer_name, :employer_url, length: { maximum: 100 }
validates :employment_title, :education, :location, length: { maximum: 100 }
validates :experience_level, numericality: { less_than_or_equal_to: 10 }, allow_blank: true
validates :text_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
validates :bg_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/, allow_blank: true
validates :website_url, :employer_url,
url: { allow_blank: true, no_local: true, schemes: %w[https http] }
validates :facebook_url,
format: /\A(http(s)?:\/\/)?(www.facebook.com|facebook.com)\/.*\Z/,
allow_blank: true
validates :stackoverflow_url,
allow_blank: true,
format:
/\A(http(s)?:\/\/)?(((www|pt|ru|es|ja).)?stackoverflow.com|(www.)?stackexchange.com)\/.*\Z/
validates :behance_url,
allow_blank: true,
format: /\A(http(s)?:\/\/)?(www.behance.net|behance.net)\/.*\Z/
validates :linkedin_url,
allow_blank: true,
format:
/\A(http(s)?:\/\/)?(www.linkedin.com|linkedin.com|[A-Za-z]{2}.linkedin.com)\/.*\Z/
validates :dribbble_url,
allow_blank: true,
format: /\A(http(s)?:\/\/)?(www.dribbble.com|dribbble.com)\/.*\Z/
validates :medium_url,
allow_blank: true,
format: /\A(http(s)?:\/\/)?(www.medium.com|medium.com)\/.*\Z/
validates :gitlab_url,
allow_blank: true,
format: /\A(http(s)?:\/\/)?(www.gitlab.com|gitlab.com)\/.*\Z/
validates :instagram_url,
allow_blank: true,
format: /\A(http(s)?:\/\/)?(?:www.)?instagram.com\/(?=.{1,30}\/?$)([a-zA-Z\d_]\.?)*[a-zA-Z\d_]+\/?\Z/
validates :twitch_url,
allow_blank: true,
format: /\A(http(s)?:\/\/)?(www.twitch.tv|twitch.tv)\/.*\Z/
validates :editor_version,
inclusion: { in: %w[v1 v2],
message: "%<value>s must be either v1 or v2" }
validates :facebook_url, length: { maximum: 1000 }, format: FACEBOOK_URL_REGEXP, allow_blank: true
validates :feed_referential_link, inclusion: { in: [true, false] }
validates :feed_url, length: { maximum: 500 }, allow_nil: true
validates :github_username, uniqueness: { allow_nil: true }, if: :github_username_changed?
validates :gitlab_url, length: { maximum: 100 }, allow_blank: true, format: GITLAB_URL_REGEXP
validates :inbox_guidelines, length: { maximum: 250 }, allow_nil: true
validates :inbox_type, inclusion: { in: INBOXES }
validates :instagram_url, length: { maximum: 100 }, allow_blank: true, format: INSTAGRAM_URL_REGEXP
validates :linkedin_url, length: { maximum: 350 }, allow_blank: true, format: LINKEDIN_URL_REGEXP
validates :mastodon_url, length: { maximum: 100 }
validates :medium_url, length: { maximum: 200 }, allow_blank: true, format: MEDIUM_URL_REGEXP
validates :mostly_work_with, :currently_learning, :currently_hacking_on, :available_for, length: { maximum: 500 }
validates :name, length: { in: 1..100 }
validates :stackoverflow_url, length: { maximum: 150 }, allow_blank: true, format: STACKOVERFLOW_URL_REGEXP
validates :summary, length: { maximum: 1300 }, allow_nil: true
validates :text_color_hex, format: COLOR_HEX_REGEXP, allow_blank: true
validates :twitch_url, length: { maximum: 100 }, allow_blank: true, format: TWITCH_URL_REGEXP
validates :twitter_username, uniqueness: { allow_nil: true }, if: :twitter_username_changed?
validates :username, presence: true, exclusion: { in: ReservedWords.all, message: MESSAGES[:invalid_username] }
validates :username, length: { in: 2..30 }, format: USERNAME_REGEXP
validates :username, uniqueness: { case_sensitive: false }, if: :username_changed?
validates :website_url, :employer_url, url: { allow_blank: true, no_local: true }
validates :website_url, length: { maximum: 100 }, allow_nil: true
validates :config_theme,
inclusion: { in: %w[default night_theme pink_theme minimal_light_theme ten_x_hacker_theme],
message: "%<value>s is not a valid theme" }
validates :config_font,
inclusion: { in: %w[default sans_serif monospace comic_sans open_dyslexic],
message: "%<value>s is not a valid font selection" }
validates :config_navbar,
inclusion: { in: %w[default static],
message: "%<value>s is not a valid navbar value" }
validates :website_url, :employer_name, :employer_url,
length: { maximum: 100 }
validates :employment_title, :education, :location,
length: { maximum: 100 }
validates :mostly_work_with, :currently_learning,
:currently_hacking_on, :available_for,
length: { maximum: 500 }
validates :inbox_type, inclusion: { in: %w[open private] }
validates :currently_streaming_on, inclusion: { in: %w[twitch] }, allow_nil: true
validates :feed_referential_link, inclusion: [true, false]
validate :conditionally_validate_summary
validate :validate_mastodon_url
validate :validate_feed_url, if: :feed_url_changed?
validate :non_banished_username, :username_changed?
validate :unique_including_orgs_and_podcasts, if: :username_changed?
validate :conditionally_validate_summary
validate :non_banished_username, :username_changed?
validate :unique_including_orgs_and_podcasts, if: :username_changed?
validate :validate_feed_url, if: :feed_url_changed?
validate :validate_mastodon_url
alias_attribute :positive_reactions_count, :reactions_count
alias_attribute :subscribed_to_welcome_notifications?, :welcome_notifications
@ -278,7 +268,6 @@ class User < ApplicationRecord
# handles both old (prefer_language_*) and new (Array of language codes) formats
def preferred_languages_array
# return @prefer_languages_array if defined? @preferred_languages_array
return @preferred_languages_array if defined?(@preferred_languages_array)
if language_settings["preferred_languages"].present?
@ -402,7 +391,13 @@ class User < ApplicationRecord
end
def unique_including_orgs_and_podcasts
errors.add(:username, "is taken.") if Organization.find_by(slug: username) || Podcast.find_by(slug: username) || Page.find_by(slug: username)
username_taken = (
Organization.exists?(slug: username) ||
Podcast.exists?(slug: username) ||
Page.exists?(slug: username)
)
errors.add(:username, "is taken.") if username_taken
end
def non_banished_username
@ -547,7 +542,7 @@ class User < ApplicationRecord
end
def temp_name_exists?
User.find_by(username: temp_username) || Organization.find_by(slug: temp_username)
User.exists?(username: temp_username) || Organization.exists?(slug: temp_username)
end
def temp_username
@ -611,8 +606,9 @@ class User < ApplicationRecord
def validate_feed_url
return if feed_url.blank?
return if RssReader.new.valid_feed_url?(feed_url)
errors.add(:feed_url, "is not a valid rss feed") unless RssReader.new.valid_feed_url?(feed_url)
errors.add(:feed_url, "is not a valid RSS/Atom feed")
end
def validate_mastodon_url
@ -623,7 +619,7 @@ class User < ApplicationRecord
errors.add(:mastodon_url, "is not an allowed Mastodon instance")
rescue URI::InvalidURIError
errors.add(:mastodon_url, "is not a valid url")
errors.add(:mastodon_url, "is not a valid URL")
end
def tag_list

View file

@ -146,7 +146,7 @@
</div>
<div class="field">
<%= f.label :company_size, "Organization Size" %>
<%= f.text_field :company_size, maxlength: 10, placeholder: "Enter a number" %>
<%= f.text_field :company_size, maxlength: 7, placeholder: "Enter a number" %>
</div>
<div class="field">
<%= f.label :story, "Our Story" %>
@ -178,7 +178,7 @@ _italic_ and **bold** is okay. Links, and headers etc. will not show up.
</div>
<div class="field">
<%= f.label :cta_button_url, "Link url" %>
<%= f.text_field :cta_button_url, placeholder: "https://example.com" %>
<%= f.text_field :cta_button_url, maxlength: 150, placeholder: "https://example.com" %>
</div>
<div class="field">
<label></label>

View file

@ -26,7 +26,7 @@
</div>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.text_field :name, maxlength: 100 %>
</div>
<%= f.label :profile_image %>
<div class="field">

View file

@ -3,8 +3,55 @@ require "rails_helper"
RSpec.describe Organization, type: :model do
let(:organization) { create(:organization) }
it { is_expected.to have_many(:sponsorships) }
it { is_expected.to have_many(:organization_memberships).dependent(:delete_all) }
describe "validations" do
describe "builtin validations" do
subject { organization }
it { is_expected.to have_many(:api_secrets).through(:users) }
it { is_expected.to have_many(:articles) }
it { is_expected.to have_many(:classified_listings) }
it { is_expected.to have_many(:collections) }
it { is_expected.to have_many(:credits) }
it { is_expected.to have_many(:display_ads) }
it { is_expected.to have_many(:notifications) }
it { is_expected.to have_many(:organization_memberships).dependent(:delete_all) }
it { is_expected.to have_many(:profile_pins) }
it { is_expected.to have_many(:sponsorships) }
it { is_expected.to have_many(:unspent_credits).class_name("Credit") }
it { is_expected.to have_many(:users).through(:organization_memberships) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:summary) }
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_presence_of(:profile_image) }
it { is_expected.to validate_presence_of(:slug) }
it { is_expected.to validate_length_of(:cta_body_markdown).is_at_most(256) }
it { is_expected.to validate_length_of(:cta_button_text).is_at_most(20) }
it { is_expected.to validate_length_of(:secret).is_equal_to(100) }
it { is_expected.to validate_length_of(:name).is_at_most(50) }
it { is_expected.to validate_length_of(:slug).is_at_least(2).is_at_most(18) }
it { is_expected.to validate_length_of(:twitter_username).is_at_most(15) }
it { is_expected.to validate_length_of(:github_username).is_at_most(50) }
it { is_expected.to validate_length_of(:url).is_at_most(200) }
it { is_expected.to validate_length_of(:tag_line).is_at_most(60) }
it { is_expected.to validate_length_of(:proof).is_at_most(1500) }
it { is_expected.to validate_length_of(:location).is_at_most(64) }
it { is_expected.to validate_length_of(:email).is_at_most(64) }
it { is_expected.to validate_length_of(:company_size).is_at_most(7) }
it { is_expected.to validate_length_of(:story).is_at_most(640) }
it { is_expected.to validate_length_of(:tech_stack).is_at_most(640) }
it { is_expected.to validate_uniqueness_of(:slug).case_insensitive }
it { is_expected.not_to allow_value("#xyz").for(:bg_color_hex) }
it { is_expected.not_to allow_value("#xyz").for(:text_color_hex) }
it { is_expected.to allow_value("#aabbcc").for(:bg_color_hex) }
it { is_expected.to allow_value("#aabbcc").for(:text_color_hex) }
it { is_expected.to allow_value("#abc").for(:bg_color_hex) }
it { is_expected.to allow_value("#abc").for(:text_color_hex) }
it { is_expected.not_to allow_value("3.0").for(:company_size) }
it { is_expected.to allow_value("3").for(:company_size) }
end
end
describe "#after_commit" do
it "on update syncs elasticsearch data" do

View file

@ -18,6 +18,8 @@ RSpec.describe User, type: :model do
describe "validations" do
describe "builtin validations" do
subject { user }
it { is_expected.to have_many(:api_secrets).dependent(:destroy) }
it { is_expected.to have_many(:articles).dependent(:destroy) }
it { is_expected.to have_many(:audit_logs).dependent(:nullify) }
@ -133,13 +135,36 @@ RSpec.describe User, type: :model do
it { is_expected.to have_one(:counters).class_name("UserCounter").dependent(:destroy) }
it { is_expected.to have_one(:pro_membership).dependent(:destroy) }
it { is_expected.to validate_uniqueness_of(:username).case_insensitive }
it { is_expected.not_to allow_value("#xyz").for(:bg_color_hex) }
it { is_expected.not_to allow_value("#xyz").for(:text_color_hex) }
it { is_expected.not_to allow_value("AcMe_1%").for(:username) }
it { is_expected.to allow_value("#aabbcc").for(:bg_color_hex) }
it { is_expected.to allow_value("#aabbcc").for(:text_color_hex) }
it { is_expected.to allow_value("#abc").for(:bg_color_hex) }
it { is_expected.to allow_value("#abc").for(:text_color_hex) }
it { is_expected.to allow_value("AcMe_1").for(:username) }
it { is_expected.to validate_inclusion_of(:inbox_type).in_array(%w[open private]) }
it { is_expected.to validate_length_of(:available_for).is_at_most(500).allow_nil }
it { is_expected.to validate_length_of(:behance_url).is_at_most(100).allow_nil }
it { is_expected.to validate_length_of(:currently_hacking_on).is_at_most(500).allow_nil }
it { is_expected.to validate_length_of(:currently_learning).is_at_most(500).allow_nil }
it { is_expected.to validate_length_of(:education).is_at_most(100).allow_nil }
it { is_expected.to validate_length_of(:email).is_at_most(50).allow_nil }
it { is_expected.to validate_length_of(:employer_name).is_at_most(100).allow_nil }
it { is_expected.to validate_length_of(:employer_url).is_at_most(100).allow_nil }
it { is_expected.to validate_length_of(:employment_title).is_at_most(100).allow_nil }
it { is_expected.to validate_length_of(:inbox_guidelines).is_at_most(250).allow_nil }
it { is_expected.to validate_length_of(:location).is_at_most(100).allow_nil }
it { is_expected.to validate_length_of(:mostly_work_with).is_at_most(500).allow_nil }
it { is_expected.to validate_length_of(:name).is_at_most(100).is_at_least(1) }
it { is_expected.to validate_length_of(:summary).is_at_most(1300).allow_nil }
it { is_expected.to validate_length_of(:username).is_at_most(30).is_at_least(2) }
it { is_expected.to validate_uniqueness_of(:github_username).allow_nil }
it { is_expected.to validate_uniqueness_of(:twitter_username).allow_nil }
it { is_expected.to validate_presence_of(:username) }
it { is_expected.to validate_length_of(:username).is_at_most(30).is_at_least(2) }
it { is_expected.to validate_length_of(:name).is_at_most(100).is_at_least(1) }
it { is_expected.to validate_inclusion_of(:inbox_type).in_array(%w[open private]) }
it { is_expected.to validate_uniqueness_of(:username).case_insensitive }
it { is_expected.to validate_url_of(:employer_url) }
it { is_expected.to validate_url_of(:website_url) }
end
it "validates username against reserved words" do
@ -828,9 +853,9 @@ RSpec.describe User, type: :model do
end
it "assigns modified username if invalid" do
OmniAuth.config.mock_auth[:twitter].info.nickname = "invalid.username"
OmniAuth.config.mock_auth[:twitter].info.nickname = "invalid.user"
new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
expect(new_user.username).to eq("invalidusername")
expect(new_user.username).to eq("invaliduser")
end
it "assigns an identity to user" do

View file

@ -15,6 +15,7 @@ require "test_prof/recipes/rspec/before_all"
require "test_prof/recipes/rspec/let_it_be"
require "test_prof/recipes/rspec/sample"
require "sidekiq/testing"
# require "validate_url/rspec_matcher"
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are

View file

@ -2,7 +2,7 @@ require "rails_helper"
RSpec.describe "UserOrganization", type: :request do
let(:user) { create(:user) }
let(:organization) { create(:organization, secret: "SECRET") }
let(:organization) { create(:organization, secret: SecureRandom.hex(50)) }
context "when joining an org" do
before { sign_in user }

View file

@ -0,0 +1,14 @@
# NOTE: this is copied from https://github.com/perfectline/validates_url/blob/master/lib/validate_url/rspec_matcher.rb
# as the recommended include mechanism doesn't work. Will remove when that's patched correctly
RSpec::Matchers.define :validate_url_of do |attribute|
match do
actual = subject.is_a?(Class) ? subject.new : subject
actual.send(:"#{attribute}=", "htp://invalidurl")
expect(actual).to be_invalid
@expected_message ||= I18n.t("errors.messages.url")
expect(actual.errors.messages[attribute.to_sym]).to include(@expected_message)
end
chain :with_message do |message|
@expected_message = message
end
end