FEATURE: phase 1 of supporting multiple email addresses

This commit is contained in:
Leo McArdle 2017-04-26 19:47:36 +01:00 committed by Guo Xiang Tan
parent 739794f0cb
commit d0b027d88d
35 changed files with 337 additions and 80 deletions

View File

@ -73,7 +73,7 @@ class Admin::EmailController < Admin::AdminController
params.require(:from) params.require(:from)
params.require(:to) params.require(:to)
# These strings aren't localized; they are sent to an anonymous SMTP user. # These strings aren't localized; they are sent to an anonymous SMTP user.
if !User.exists?(email: Email.downcase(params[:from])) && !SiteSetting.enable_staged_users if !User.with_email(Email.downcase(params[:from])).exists? && !SiteSetting.enable_staged_users
render json: { reject: true, reason: "Mail from your address is not accepted. Do you have an account here?" } render json: { reject: true, reason: "Mail from your address is not accepted. Do you have an account here?" }
elsif Email::Receiver.check_address(Email.downcase(params[:to])).nil? elsif Email::Receiver.check_address(Email.downcase(params[:to])).nil?
render json: { reject: true, reason: "Mail to this address is not accepted. Check the address and try to send again?" } render json: { reject: true, reason: "Mail to this address is not accepted. Check the address and try to send again?" }

View File

@ -29,7 +29,11 @@ class Admin::GroupsController < Admin::AdminController
users = (params[:users] || []).map {|u| u.downcase} users = (params[:users] || []).map {|u| u.downcase}
valid_emails = {} valid_emails = {}
valid_usernames = {} valid_usernames = {}
valid_users = User.where("username_lower IN (:users) OR email IN (:users)", users: users).pluck(:id, :username_lower, :email)
valid_users = User.joins(:user_emails)
.where("username_lower IN (:users) OR user_emails.email IN (:users)", users: users)
.pluck(:id, :username_lower, :"user_emails.email")
valid_users.map! do |id, username_lower, email| valid_users.map! do |id, username_lower, email|
valid_emails[email] = valid_usernames[username_lower] = id valid_emails[email] = valid_usernames[username_lower] = id
id id

View File

@ -15,7 +15,7 @@ class FinishInstallationController < ApplicationController
email = params[:email].strip email = params[:email].strip
raise Discourse::InvalidParameters.new unless @allowed_emails.include?(email) raise Discourse::InvalidParameters.new unless @allowed_emails.include?(email)
return redirect_confirm(email) if User.where(email: email).exists? return redirect_confirm(email) if UserEmail.exists?(email: email)
@user.email = email @user.email = email
@user.username = params[:username] @user.username = params[:username]
@ -37,7 +37,7 @@ class FinishInstallationController < ApplicationController
def resend_email def resend_email
@email = session[:registered_email] @email = session[:registered_email]
@user = User.where(email: @email).first @user = User.find_by_email(@email)
if @user.present? if @user.present?
@email_token = @user.email_tokens.unconfirmed.active.first @email_token = @user.email_tokens.unconfirmed.active.first
if @email_token.present? if @email_token.present?

View File

@ -153,7 +153,7 @@ class GroupsController < ApplicationController
elsif params[:user_ids].present? elsif params[:user_ids].present?
User.find(params[:user_ids].split(",")) User.find(params[:user_ids].split(","))
elsif params[:user_emails].present? elsif params[:user_emails].present?
User.where(email: params[:user_emails].split(",")) User.with_email(params[:user_emails].split(","))
else else
raise Discourse::InvalidParameters.new( raise Discourse::InvalidParameters.new(
'user_ids or usernames or user_emails must be present' 'user_ids or usernames or user_emails must be present'

View File

@ -311,7 +311,7 @@ class UsersController < ApplicationController
return fail_with("login.reserved_username") return fail_with("login.reserved_username")
end end
if user = User.find_by(staged: true, email: params[:email].strip.downcase) if user = User.where(staged: true).with_email(params[:email].strip.downcase).first
user_params.each { |k, v| user.send("#{k}=", v) } user_params.each { |k, v| user.send("#{k}=", v) }
user.staged = false user.staged = false
else else
@ -490,7 +490,7 @@ class UsersController < ApplicationController
RateLimiter.new(nil, "admin-login-hr-#{request.remote_ip}", 6, 1.hour).performed! RateLimiter.new(nil, "admin-login-hr-#{request.remote_ip}", 6, 1.hour).performed!
RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed! RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed!
user = User.where(email: params[:email], admin: true).human_users.first user = User.with_email(params[:email]).where(admin: true).human_users.first
if user if user
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create(email: user.email)
Jobs.enqueue(:critical_user_email, type: :admin_login, user_id: user.id, email_token: email_token.token) Jobs.enqueue(:critical_user_email, type: :admin_login, user_id: user.id, email_token: email_token.token)
@ -606,6 +606,7 @@ class UsersController < ApplicationController
User.transaction do User.transaction do
@user.email = params[:email] @user.email = params[:email]
if @user.save if @user.save
@user.email_tokens.create(email: @user.email) @user.email_tokens.create(email: @user.email)
enqueue_activation_email enqueue_activation_email

View File

@ -13,7 +13,8 @@ module Jobs
domains = group.automatic_membership_email_domains.gsub('.', '\.') domains = group.automatic_membership_email_domains.gsub('.', '\.')
User.where("email ~* '@(#{domains})$'") User.joins(:user_emails)
.where("user_emails.email ~* '@(#{domains})$'")
.where("users.id NOT IN (SELECT user_id FROM group_users WHERE group_users.group_id = ?)", group_id) .where("users.id NOT IN (SELECT user_id FROM group_users WHERE group_users.group_id = ?)", group_id)
.activated .activated
.where(staged: false) .where(staged: false)

View File

@ -71,7 +71,7 @@ class EmailToken < ActiveRecord::Base
end end
if user if user
return User.find_by(email: Email.downcase(user.email)) if Invite.redeem_from_email(user.email).present? return User.find_by_email(user.email) if Invite.redeem_from_email(user.email).present?
user user
end end
end end

View File

@ -595,7 +595,7 @@ end
# allow_membership_requests :boolean default(FALSE), not null # allow_membership_requests :boolean default(FALSE), not null
# full_name :string # full_name :string
# default_notification_level :integer default(3), not null # default_notification_level :integer default(3), not null
# visibility_level :integer default(0) # visibility_level :integer default(0), not null
# #
# Indexes # Indexes
# #

View File

@ -32,8 +32,9 @@ class Invite < ActiveRecord::Base
def user_doesnt_already_exist def user_doesnt_already_exist
@email_already_exists = false @email_already_exists = false
return if email.blank? return if email.blank?
u = User.find_by("email = ?", Email.downcase(email)) user = User.find_by_email(email)
if u && u.id != self.user_id
if user && user.id != self.user_id
@email_already_exists = true @email_already_exists = true
errors.add(:email) errors.add(:email)
end end
@ -98,11 +99,9 @@ class Invite < ActiveRecord::Base
group_ids = opts[:group_ids] group_ids = opts[:group_ids]
send_email = opts[:send_email].nil? ? true : opts[:send_email] send_email = opts[:send_email].nil? ? true : opts[:send_email]
custom_message = opts[:custom_message] custom_message = opts[:custom_message]
lower_email = Email.downcase(email) lower_email = Email.downcase(email)
user = User.find_by(email: lower_email)
if user if user = User.find_by_email(lower_email)
extend_permissions(topic, user, invited_by) if topic extend_permissions(topic, user, invited_by) if topic
raise UserExists.new I18n.t("invite.user_exists", email: lower_email, username: user.username) raise UserExists.new I18n.t("invite.user_exists", email: lower_email, username: user.username)
end end

View File

@ -91,7 +91,7 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f
end end
def get_existing_user def get_existing_user
User.where(admin: false).find_by(email: invite.email) User.where(admin: false).find_by_email(invite.email)
end end

View File

@ -55,3 +55,16 @@ class SearchLog < ActiveRecord::Base
end end
end end
end end
# == Schema Information
#
# Table name: search_logs
#
# id :integer not null, primary key
# term :string not null
# user_id :integer
# ip_address :inet not null
# clicked_topic_id :integer
# search_type :integer not null
# created_at :datetime not null
#

View File

@ -810,7 +810,7 @@ SQL
end end
def grant_permission_to_user(lower_email) def grant_permission_to_user(lower_email)
user = User.find_by(email: lower_email) user = User.find_by_email(lower_email)
topic_allowed_users.create!(user_id: user.id) topic_allowed_users.create!(user_id: user.id)
end end

View File

@ -42,7 +42,11 @@ class User < ActiveRecord::Base
has_many :email_change_requests, dependent: :destroy has_many :email_change_requests, dependent: :destroy
has_many :directory_items, dependent: :delete_all has_many :directory_items, dependent: :delete_all
has_many :user_auth_tokens, dependent: :destroy has_many :user_auth_tokens, dependent: :destroy
has_many :user_emails, dependent: :destroy
has_one :primary_email, -> { where(primary: true) },
dependent: :destroy,
class_name: 'UserEmail'
has_one :user_option, dependent: :destroy has_one :user_option, dependent: :destroy
has_one :user_avatar, dependent: :destroy has_one :user_avatar, dependent: :destroy
@ -74,16 +78,13 @@ class User < ActiveRecord::Base
delegate :last_sent_email_address, :to => :email_logs delegate :last_sent_email_address, :to => :email_logs
before_validation :strip_downcase_email
validates_presence_of :username validates_presence_of :username
validate :username_validator, if: :username_changed? validate :username_validator, if: :username_changed?
validates :email, presence: true, uniqueness: true
validates :email, format: { with: EmailValidator.email_regex }, if: :email_changed?
validates :email, email: true, if: :should_validate_email?
validate :password_validator validate :password_validator
validates :name, user_full_name: true, if: :name_changed?, length: { maximum: 255 } validates :name, user_full_name: true, if: :name_changed?, length: { maximum: 255 }
validates :ip_address, allowed_ip_address: {on: :create, message: :signup_not_allowed} validates :ip_address, allowed_ip_address: {on: :create, message: :signup_not_allowed}
validates :primary_email, presence: true, if: :should_validate_primary_email?
validates_associated :primary_email, if: :should_validate_primary_email?
after_initialize :add_trust_level after_initialize :add_trust_level
@ -123,6 +124,8 @@ class User < ActiveRecord::Base
# set to true to optimize creation and save for imports # set to true to optimize creation and save for imports
attr_accessor :import_mode attr_accessor :import_mode
scope :with_email, ->(email) { joins(:user_emails).where(user_emails: { email: email }) }
scope :human_users, -> { where('users.id > 0') } scope :human_users, -> { where('users.id > 0') }
# excluding fake users like the system user or anonymous users # excluding fake users like the system user or anonymous users
@ -232,7 +235,7 @@ class User < ActiveRecord::Base
end end
def self.find_by_email(email) def self.find_by_email(email)
find_by(email: Email.downcase(email)) self.with_email(Email.downcase(email)).first
end end
def self.find_by_username(username) def self.find_by_username(username)
@ -267,8 +270,8 @@ class User < ActiveRecord::Base
used_invite.try(:invited_by) used_invite.try(:invited_by)
end end
def should_validate_email? def should_validate_primary_email?
return !skip_email_validation && !staged? && email_changed? !skip_email_validation && !staged?
end end
# Approve this user # Approve this user
@ -935,6 +938,18 @@ class User < ActiveRecord::Base
end end
end end
def email
primary_email.email
end
def email=(email)
if primary_email
self.new_record? ? primary_email.email = email : primary_email.update(email: email)
else
build_primary_email(email: email)
end
end
protected protected
def badge_grant def badge_grant
@ -1011,13 +1026,6 @@ class User < ActiveRecord::Base
self.username_lower = username.downcase self.username_lower = username.downcase
end end
def strip_downcase_email
if self.email
self.email = self.email.strip
self.email = self.email.downcase
end
end
def username_validator def username_validator
username_format_validator || begin username_format_validator || begin
lower = username.downcase lower = username.downcase
@ -1104,7 +1112,7 @@ end
# name :string # name :string
# seen_notification_id :integer default(0), not null # seen_notification_id :integer default(0), not null
# last_posted_at :datetime # last_posted_at :datetime
# email :string(513) not null # email :string(513)
# password_hash :string(64) # password_hash :string(64)
# salt :string(32) # salt :string(32)
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null

45
app/models/user_email.rb Normal file
View File

@ -0,0 +1,45 @@
require_dependency 'email_validator'
class UserEmail < ActiveRecord::Base
belongs_to :user
before_validation :strip_downcase_email
validates :email, presence: true, uniqueness: true
validates :email, email: true, format: { with: EmailValidator.email_regex },
if: :skip_email_validation?
validates :primary, uniqueness: { scope: [:user_id] }
private
def strip_downcase_email
if self.email
self.email = self.email.strip
self.email = self.email.downcase
end
end
def skip_email_validation?
return true if user && user.skip_email_validation
email_changed?
end
end
# == Schema Information
#
# Table name: user_emails
#
# id :integer not null, primary key
# user_id :integer not null
# email :string(513) not null
# primary :boolean default(FALSE), not null
# created_at :datetime
# updated_at :datetime
#
# Indexes
#
# index_user_emails_on_user_id (user_id)
# index_user_emails_on_user_id_and_primary (user_id,primary) UNIQUE
#

View File

@ -6,12 +6,18 @@ if user
user.save user.save
end end
UserEmail.seed do |ue|
ue.id = -1
ue.email = "no_email"
ue.primary = true
ue.user_id = -1
end
User.seed do |u| User.seed do |u|
u.id = -1 u.id = -1
u.name = "system" u.name = "system"
u.username = "system" u.username = "system"
u.username_lower = "system" u.username_lower = "system"
u.email = "no_email"
u.password = SecureRandom.hex u.password = SecureRandom.hex
u.active = true u.active = true
u.admin = true u.admin = true
@ -56,12 +62,18 @@ ColumnDropper.drop(
# User for the smoke tests # User for the smoke tests
if ENV["SMOKE"] == "1" if ENV["SMOKE"] == "1"
UserEmail.seed do |ue|
ue.id = 0
ue.email = "smoke_user@discourse.org"
ue.primary = true
ue.user_id = 0
end
smoke_user = User.seed do |u| smoke_user = User.seed do |u|
u.id = 0 u.id = 0
u.name = "smoke_user" u.name = "smoke_user"
u.username = "smoke_user" u.username = "smoke_user"
u.username_lower = "smoke_user" u.username_lower = "smoke_user"
u.email = "smoke_user@discourse.org"
u.password = "P4ssw0rd" u.password = "P4ssw0rd"
u.active = true u.active = true
u.approved = true u.approved = true
@ -77,4 +89,3 @@ if ENV["SMOKE"] == "1"
EmailToken.where(user_id: smoke_user.id).update_all(confirmed: true) EmailToken.where(user_id: smoke_user.id).update_all(confirmed: true)
end end

View File

@ -0,0 +1,42 @@
require_dependency 'column_dropper'
class CreateUserEmails < ActiveRecord::Migration
def up
create_table :user_emails do |t|
t.integer :user_id, null: false
t.string :email, limit: 513, null: false
t.boolean :primary, default: false, null: false
t.timestamps
end
add_index :user_emails, :user_id
add_index :user_emails, [:user_id, :primary], unique: true
execute "CREATE UNIQUE INDEX index_user_emails_on_email ON user_emails (lower(email));"
execute <<~SQL
INSERT INTO user_emails (
id,
user_id,
email,
"primary",
created_at,
updated_at
) SELECT
id,
id,
email,
'TRUE',
created_at,
updated_at
FROM users
SQL
change_column_null :users, :email, true
ColumnDropper.mark_readonly(:users, :email)
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -4,7 +4,7 @@ class AdminUserIndexQuery
def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels) def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels)
@params = params @params = params
@query = initialize_query_with_order(klass) @query = initialize_query_with_order(klass).joins(:user_emails)
@trust_levels = trust_levels @trust_levels = trust_levels
end end
@ -52,7 +52,7 @@ class AdminUserIndexQuery
if !custom_order.present? if !custom_order.present?
if params[:query] == "active" if params[:query] == "active"
order << "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD')) DESC" order << "COALESCE(users.last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD')) DESC"
else else
order << "users.created_at DESC" order << "users.created_at DESC"
end end
@ -106,7 +106,7 @@ class AdminUserIndexQuery
if ip = IPAddr.new(params[:filter]) rescue nil if ip = IPAddr.new(params[:filter]) rescue nil
@query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s) @query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s)
else else
@query.where('username_lower ILIKE :filter OR email ILIKE :filter', filter: "%#{params[:filter]}%") @query.where('username_lower ILIKE :filter OR user_emails.email ILIKE :filter', filter: "%#{params[:filter]}%")
end end
end end
end end
@ -119,7 +119,7 @@ class AdminUserIndexQuery
def filter_exclude def filter_exclude
if params[:exclude].present? if params[:exclude].present?
@query.where('id != ?', params[:exclude]) @query.where('users.id != ?', params[:exclude])
end end
end end

View File

@ -12,7 +12,7 @@ class AvatarLookup
private private
def self.lookup_columns def self.lookup_columns
@lookup_columns ||= %i{id email username uploaded_avatar_id} @lookup_columns ||= %i{id user_emails.email username uploaded_avatar_id}
end end
def users def users
@ -22,7 +22,8 @@ class AvatarLookup
def user_lookup_hash def user_lookup_hash
# adding tap here is a personal taste thing # adding tap here is a personal taste thing
hash = {} hash = {}
User.where(:id => @user_ids) User.joins(:user_emails)
.where(id: @user_ids)
.select(AvatarLookup.lookup_columns) .select(AvatarLookup.lookup_columns)
.each{ |user| hash[user.id] = user } .each{ |user| hash[user.id] = user }
hash hash

View File

@ -400,7 +400,7 @@ module BackupRestore
end end
def notify_user def notify_user
if user = User.find_by(email: @user_info[:email]) if user = User.find_by_email(@user_info[:email])
log "Notifying '#{user.username}' of the end of the restore..." log "Notifying '#{user.username}' of the end of the restore..."
status = @success ? :restore_succeeded : :restore_failed status = @success ? :restore_succeeded : :restore_failed

View File

@ -8,7 +8,7 @@ class ColumnDropper
delay ||= Rails.env.production? ? 60 : 0 delay ||= Rails.env.production? ? 60 : 0
sql = <<SQL sql = <<~SQL
SELECT 1 SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = 'public' AND WHERE table_schema = 'public' AND
@ -32,7 +32,41 @@ SQL
columns.each do |column| columns.each do |column|
# safe cause it is protected on method entry, can not be passed in params # safe cause it is protected on method entry, can not be passed in params
ActiveRecord::Base.exec_sql("ALTER TABLE #{table} DROP COLUMN IF EXISTS #{column}") ActiveRecord::Base.exec_sql("ALTER TABLE #{table} DROP COLUMN IF EXISTS #{column}")
ActiveRecord::Base.exec_sql <<~SQL
DROP FUNCTION IF EXISTS #{readonly_function_name(table, column)};
DROP TRIGGER IF EXISTS #{readonly_trigger_name(table, column)} ON #{table};
SQL
end end
end end
end end
def self.mark_readonly(table_name, column_name)
ActiveRecord::Base.exec_sql <<-SQL
CREATE OR REPLACE FUNCTION #{readonly_function_name(table_name, column_name)} RETURNS trigger AS $rcr$
BEGIN
RAISE EXCEPTION 'Discourse: #{column_name} in #{table_name} is readonly';
END
$rcr$ LANGUAGE plpgsql;
SQL
ActiveRecord::Base.exec_sql <<-SQL
CREATE TRIGGER #{readonly_trigger_name(table_name, column_name)}
BEFORE INSERT OR UPDATE OF #{column_name}
ON #{table_name}
FOR EACH ROW
WHEN (NEW.#{column_name} IS NOT NULL)
EXECUTE PROCEDURE #{readonly_function_name(table_name, column_name)};
SQL
end
private
def self.readonly_function_name(table_name, column_name)
"raise_#{table_name}_#{column_name}_readonly()"
end
def self.readonly_trigger_name(table_name, column_name)
"#{table_name}_#{column_name}_readonly"
end
end end

View File

@ -153,7 +153,7 @@ module Email
if $redis.setnx(key, "1") if $redis.setnx(key, "1")
$redis.expire(key, 25.hours) $redis.expire(key, 25.hours)
if user = User.find_by(email: email) if user = User.find_by_email(email)
user.user_stat.bounce_score += score user.user_stat.bounce_score += score
user.user_stat.reset_bounce_score_after = SiteSetting.reset_bounce_score_after_days.days.from_now user.user_stat.reset_bounce_score_after = SiteSetting.reset_bounce_score_after_days.days.from_now
user.user_stat.save user.user_stat.save

View File

@ -81,7 +81,7 @@ class EmailUpdater
confirm_result = :authorizing_new confirm_result = :authorizing_new
when EmailChangeRequest.states[:authorizing_new] when EmailChangeRequest.states[:authorizing_new]
change_req.update_column(:change_state, EmailChangeRequest.states[:complete]) change_req.update_column(:change_state, EmailChangeRequest.states[:complete])
user.update_column(:email, token.email) user.primary_email.update_column(:email, token.email)
confirm_result = :complete confirm_result = :complete
end end
else else

View File

@ -18,7 +18,7 @@ module ImportExport
def import_users def import_users
@export_data[:users].each do |u| @export_data[:users].each do |u|
existing = User.where(email: u[:email]).first existing = User.with_email(email: u[:email]).first
if existing if existing
if existing.custom_fields["import_id"] != u[:id] if existing.custom_fields["import_id"] != u[:id]
existing.custom_fields["import_id"] = u[:id] existing.custom_fields["import_id"] = u[:id]

View File

@ -4,12 +4,18 @@ user = User.find_by(id: -2)
if !user if !user
suggested_username = UserNameSuggester.suggest(discobot_username) suggested_username = UserNameSuggester.suggest(discobot_username)
UserEmail.seed do |ue|
ue.id = -2
ue.email = "discobot_email"
ue.primary = true
ue.user_id = -2
end
User.seed do |u| User.seed do |u|
u.id = -2 u.id = -2
u.name = discobot_username u.name = discobot_username
u.username = suggested_username u.username = suggested_username
u.username_lower = suggested_username.downcase u.username_lower = suggested_username.downcase
u.email = "discobot_email"
u.password = SecureRandom.hex u.password = SecureRandom.hex
u.active = true u.active = true
u.approved = true u.approved = true

View File

@ -93,7 +93,7 @@ class BulkImport::Base
puts "Loading users indexes..." puts "Loading users indexes..."
@last_user_id = User.unscoped.maximum(:id) @last_user_id = User.unscoped.maximum(:id)
@emails = User.unscoped.pluck(:email).to_set @emails = User.unscoped.pluck(:"user_emails.email").to_set
@usernames_lower = User.unscoped.pluck(:username_lower).to_set @usernames_lower = User.unscoped.pluck(:username_lower).to_set
@mapped_usernames = UserCustomField.joins(:user).where(name: "import_username").pluck("user_custom_fields.value", "users.username").to_h @mapped_usernames = UserCustomField.joins(:user).where(name: "import_username").pluck("user_custom_fields.value", "users.username").to_h

View File

@ -324,7 +324,7 @@ class ImportScripts::Base
rescue => e rescue => e
# try based on email # try based on email
if e.try(:record).try(:errors).try(:messages).try(:[], :email).present? if e.try(:record).try(:errors).try(:messages).try(:[], :email).present?
if existing = User.find_by(email: opts[:email].downcase) if existing = User.find_by_email(opts[:email].downcase)
existing.custom_fields["import_id"] = import_id existing.custom_fields["import_id"] = import_id
existing.save! existing.save!
u = existing u = existing

View File

@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
require 'column_dropper' require 'column_dropper'
describe ColumnDropper do RSpec.describe ColumnDropper do
def has_column?(table, column) def has_column?(table, column)
Topic.exec_sql("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS Topic.exec_sql("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
@ -48,5 +48,78 @@ describe ColumnDropper do
expect(dropped_proc_called).to eq(true) expect(dropped_proc_called).to eq(true)
end end
describe '.mark_readonly' do
let(:table_name) { "table_with_readonly_column" }
before do
ActiveRecord::Base.exec_sql <<~SQL
CREATE TABLE #{table_name} (topic_id INTEGER, email TEXT);
INSERT INTO #{table_name} (topic_id, email)
VALUES (1, 'something@email.com');
SQL
described_class.mark_readonly(table_name, 'email')
end end
after do
ActiveRecord::Base.exec_sql <<~SQL
DROP TABLE IF EXISTS #{table_name};
DROP TRIGGER IF EXISTS #{table_name}_email_readonly ON #{table_name};
SQL
end
it 'should prevent updates to the readonly column' do
expect do
ActiveRecord::Base.exec_sql <<~SQL
UPDATE #{table_name}
SET email = 'testing@email.com'
WHERE topic_id = 1;
SQL
end.to raise_error(
PG::RaiseException,
/Discourse: email in #{table_name} is readonly/
)
ActiveRecord::Base.exec_sql("ROLLBACK")
end
it 'should allow updates to the other columns' do
ActiveRecord::Base.exec_sql <<~SQL
UPDATE #{table_name}
SET topic_id = 2
WHERE topic_id = 1
SQL
expect(
ActiveRecord::Base.exec_sql("SELECT * FROM #{table_name};").values
).to include(["2", "something@email.com"])
end
it 'should prevent insertions to the readonly column' do
expect do
ActiveRecord::Base.exec_sql <<~SQL
INSERT INTO #{table_name} (topic_id, email)
VALUES (2, 'something@email.com');
SQL
end.to raise_error(
PG::RaiseException,
/Discourse: email in table_with_readonly_column is readonly/
)
ActiveRecord::Base.exec_sql("ROLLBACK")
end
it 'should allow insertions to the other columns' do
ActiveRecord::Base.exec_sql <<~SQL
INSERT INTO #{table_name} (topic_id)
VALUES (2);
SQL
expect(
ActiveRecord::Base.exec_sql("SELECT * FROM #{table_name} WHERE topic_id = 2;").values
).to include(["2", nil])
end
end
end

View File

@ -381,7 +381,7 @@ describe Email::Receiver do
it "invites everyone in the chain but emails configured as 'incoming' (via reply, group or category)" do it "invites everyone in the chain but emails configured as 'incoming' (via reply, group or category)" do
expect { process(:cc) }.to change(Topic, :count) expect { process(:cc) }.to change(Topic, :count)
emails = Topic.last.allowed_users.pluck(:email) emails = Topic.last.allowed_users.joins(:user_emails).pluck(:"user_emails.email")
expect(emails.size).to eq(3) expect(emails.size).to eq(3)
expect(emails).to include("someone@else.com", "discourse@bar.com", "wat@bar.com") expect(emails).to include("someone@else.com", "discourse@bar.com", "wat@bar.com")
end end

View File

@ -154,6 +154,10 @@ describe Admin::UsersController do
@another_user = Fabricate(:coding_horror) @another_user = Fabricate(:coding_horror)
end end
after do
$redis.flushall
end
it "raises an error when the user doesn't have permission" do it "raises an error when the user doesn't have permission" do
Guardian.any_instance.expects(:can_grant_admin?).with(@another_user).returns(false) Guardian.any_instance.expects(:can_grant_admin?).with(@another_user).returns(false)
xhr :put, :grant_admin, user_id: @another_user.id xhr :put, :grant_admin, user_id: @another_user.id
@ -512,7 +516,7 @@ describe Admin::UsersController do
xhr :post, :invite_admin, name: 'Bill', username: 'bill22', email: 'bill@bill.com' xhr :post, :invite_admin, name: 'Bill', username: 'bill22', email: 'bill@bill.com'
expect(response).to be_success expect(response).to be_success
u = User.find_by(email: 'bill@bill.com') u = User.find_by_email('bill@bill.com')
expect(u.name).to eq("Bill") expect(u.name).to eq("Bill")
expect(u.username).to eq("bill22") expect(u.username).to eq("bill22")
expect(u.admin).to eq(true) expect(u.admin).to eq(true)
@ -587,7 +591,7 @@ describe Admin::UsersController do
xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload) xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload)
expect(response).to be_success expect(response).to be_success
user = User.where(email: 'dr@claw.com').first user = User.find_by_email('dr@claw.com')
expect(user).to be_present expect(user).to be_present
expect(user.ip_address).to be_blank expect(user.ip_address).to be_blank
end end
@ -599,7 +603,7 @@ describe Admin::UsersController do
xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload) xhr :post, :sync_sso, Rack::Utils.parse_query(sso.payload)
expect(response.status).to eq(403) expect(response.status).to eq(403)
expect(JSON.parse(response.body)["message"]).to include("Email can't be blank") expect(JSON.parse(response.body)["message"]).to include("Primary email is invalid")
end end
end end
end end

View File

@ -822,7 +822,7 @@ describe UsersController do
it "should succeed without the optional field" do it "should succeed without the optional field" do
xhr :post, :create, create_params xhr :post, :create, create_params
expect(response).to be_success expect(response).to be_success
inserted = User.where(email: @user.email).first inserted = User.find_by_email(@user.email)
expect(inserted).to be_present expect(inserted).to be_present
expect(inserted.custom_fields).to be_present expect(inserted.custom_fields).to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq('value1') expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq('value1')
@ -834,7 +834,7 @@ describe UsersController do
create_params[:user_fields][optional_field.id.to_s] = 'value3' create_params[:user_fields][optional_field.id.to_s] = 'value3'
xhr :post, :create, create_params.merge(create_params) xhr :post, :create, create_params.merge(create_params)
expect(response).to be_success expect(response).to be_success
inserted = User.where(email: @user.email).first inserted = User.find_by_email(@user.email)
expect(inserted).to be_present expect(inserted).to be_present
expect(inserted.custom_fields).to be_present expect(inserted.custom_fields).to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq('value1') expect(inserted.custom_fields["user_field_#{user_field.id}"]).to eq('value1')
@ -846,7 +846,7 @@ describe UsersController do
create_params[:user_fields][optional_field.id.to_s] = ('x' * 3000) create_params[:user_fields][optional_field.id.to_s] = ('x' * 3000)
xhr :post, :create, create_params.merge(create_params) xhr :post, :create, create_params.merge(create_params)
expect(response).to be_success expect(response).to be_success
inserted = User.where(email: @user.email).first inserted = User.find_by_email(@user.email)
val = inserted.custom_fields["user_field_#{optional_field.id}"] val = inserted.custom_fields["user_field_#{optional_field.id}"]
expect(val.length).to eq(UserField.max_length) expect(val.length).to eq(UserField.max_length)
@ -868,7 +868,7 @@ describe UsersController do
it "should succeed" do it "should succeed" do
xhr :post, :create, create_params xhr :post, :create, create_params
expect(response).to be_success expect(response).to be_success
inserted = User.where(email: @user.email).first inserted = User.find_by_email(@user.email)
expect(inserted).to be_present expect(inserted).to be_present
expect(inserted.custom_fields).not_to be_present expect(inserted.custom_fields).not_to be_present
expect(inserted.custom_fields["user_field_#{user_field.id}"]).to be_blank expect(inserted.custom_fields["user_field_#{user_field.id}"]).to be_blank
@ -883,7 +883,7 @@ describe UsersController do
xhr :post, :create, email: staged.email, username: "zogstrip", password: "P4ssw0rd$$" xhr :post, :create, email: staged.email, username: "zogstrip", password: "P4ssw0rd$$"
result = ::JSON.parse(response.body) result = ::JSON.parse(response.body)
expect(result["success"]).to eq(true) expect(result["success"]).to eq(true)
expect(User.find_by(email: staged.email).staged).to eq(false) expect(User.find_by_email(staged.email).staged).to eq(false)
end end
end end
@ -2033,6 +2033,7 @@ describe UsersController do
password: 'qwerqwer123', password: 'qwerqwer123',
email: user.email email: user.email
} }
expect(response).to_not be_success expect(response).to_not be_success
end end

View File

@ -0,0 +1,4 @@
Fabricator(:user_email) do
email { sequence(:email) { |i| "bruce#{i}@wayne.com" } }
primary true
end

View File

@ -315,6 +315,13 @@ describe "Groups" do
expect(response).to be_success expect(response).to be_success
end end
it "adds by email" do
expect { xhr :put, "/groups/#{group.id}/members", user_emails: [user1.email, user2.email].join(",") }
.to change { group.users.count }.by(2)
expect(response).to be_success
end
end end
it "returns 422 if member already exists" do it "returns 422 if member already exists" do

View File

@ -271,7 +271,7 @@ describe DiscourseSingleSignOn do
user = sso.lookup_or_create_user(ip_address) user = sso.lookup_or_create_user(ip_address)
expect(user.active).to eq(true) expect(user.active).to eq(true)
user.update_columns(email: 'xXx@themovie.com') user.primary_email.update_columns(email: 'xXx@themovie.com')
user = sso.lookup_or_create_user(ip_address) user = sso.lookup_or_create_user(ip_address)
expect(user.email).to eq(old_email) expect(user.email).to eq(old_email)

View File

@ -9,11 +9,10 @@ describe User do
describe 'emails' do describe 'emails' do
let(:user) { Fabricate.build(:user) } let(:user) { Fabricate.build(:user) }
it { is_expected.to validate_presence_of :email }
describe 'when record has a valid email' do describe 'when record has a valid email' do
it "should be valid" do it "should be valid" do
user.email = 'test@gmail.com' user.email = 'test@gmail.com'
expect(user).to be_valid expect(user).to be_valid
end end
end end
@ -21,7 +20,9 @@ describe User do
describe 'when record has an invalid email' do describe 'when record has an invalid email' do
it 'should not be valid' do it 'should not be valid' do
user.email = 'test@gmailcom' user.email = 'test@gmailcom'
expect(user).to_not be_valid expect(user).to_not be_valid
expect(user.errors.messages).to include(:primary_email)
end end
end end
end end

View File

@ -38,8 +38,10 @@ describe BadgeGranter do
describe 'preview' do describe 'preview' do
it 'can correctly preview' do it 'can correctly preview' do
Fabricate(:user, email: 'sam@gmail.com') Fabricate(:user, email: 'sam@gmail.com')
result = BadgeGranter.preview('select id user_id, null post_id, created_at granted_at from users result = BadgeGranter.preview('select u.id user_id, null post_id, u.created_at granted_at from users u
where email like \'%gmail.com\'', explain: true) join user_emails ue on ue.user_id = u.id AND ue.primary
where ue.email like \'%gmail.com\'', explain: true)
expect(result[:grant_count]).to eq(1) expect(result[:grant_count]).to eq(1)
expect(result[:query_plan]).to be_present expect(result[:query_plan]).to be_present
end end