552 lines
19 KiB
Ruby
552 lines
19 KiB
Ruby
#
|
|
# Copyright (C) 2011 Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
require File.expand_path(File.dirname(__FILE__) + '/../sharding_spec_helper')
|
|
|
|
describe CommunicationChannel do
|
|
before(:each) do
|
|
@pseudonym = mock('Pseudonym')
|
|
@pseudonym.stubs(:destroyed?).returns(false)
|
|
Pseudonym.stubs(:find_by_user_id).returns(@pseudonym)
|
|
end
|
|
|
|
it "should create a new instance given valid attributes" do
|
|
factory_with_protected_attributes(CommunicationChannel, communication_channel_valid_attributes)
|
|
end
|
|
|
|
describe 'imported?' do
|
|
it 'should default to false' do
|
|
expect(CommunicationChannel.new).not_to be_imported
|
|
end
|
|
|
|
it 'should be false if the channel has no pseudonym' do
|
|
communication_channel_model
|
|
expect(@communication_channel).not_to be_imported
|
|
end
|
|
|
|
it 'should be false if the channel is associated with a pseudonym' do
|
|
user_with_pseudonym(:active_all => true)
|
|
channel = @pseudonym.communication_channel
|
|
|
|
expect(channel).not_to be_imported
|
|
end
|
|
|
|
it "should be true if the channel is the sis_communication_channel of a pseudonym" do
|
|
user_with_pseudonym(:active_all => true)
|
|
channel = @pseudonym.communication_channel
|
|
@pseudonym.update_attribute(:sis_communication_channel_id, channel.id)
|
|
|
|
expect(channel).to be_imported
|
|
end
|
|
end
|
|
|
|
it "should have a decent state machine" do
|
|
communication_channel_model
|
|
expect(@cc.state).to eql(:unconfirmed)
|
|
@cc.confirm
|
|
expect(@cc.state).to eql(:active)
|
|
@cc.retire
|
|
expect(@cc.state).to eql(:retired)
|
|
@cc.re_activate
|
|
expect(@cc.state).to eql(:active)
|
|
|
|
communication_channel_model(:path => "another_path@example.com")
|
|
expect(@cc.state).to eql(:unconfirmed)
|
|
@cc.retire
|
|
expect(@cc.state).to eql(:retired)
|
|
@cc.re_activate
|
|
expect(@cc.state).to eql(:active)
|
|
end
|
|
|
|
it "should reset the bounce count when being reactivated" do
|
|
communication_channel_model
|
|
@cc.confirm
|
|
@cc.retire
|
|
@cc.bounce_count = 2
|
|
@cc.save!
|
|
@cc.re_activate
|
|
@cc.reload
|
|
expect(@cc.bounce_count).to eq(0)
|
|
end
|
|
|
|
it "should set a confirmation code unless one has been set" do
|
|
CanvasSlug.expects(:generate).at_least(1).returns('abc123')
|
|
communication_channel_model
|
|
expect(@cc.confirmation_code).to eql('abc123')
|
|
end
|
|
|
|
it "should be able to reset a confirmation code" do
|
|
communication_channel_model
|
|
old_cc = @cc.confirmation_code
|
|
@cc.set_confirmation_code(true)
|
|
expect(@cc.confirmation_code).not_to eql(old_cc)
|
|
end
|
|
|
|
it "should use a 15-digit confirmation code for default or email path_type settings" do
|
|
communication_channel_model
|
|
expect(@cc.path_type).to eql('email')
|
|
expect(@cc.confirmation_code.size).to eql(25)
|
|
end
|
|
|
|
it "should use a 4-digit confirmation_code for settings other than email" do
|
|
communication_channel_model
|
|
@cc.path_type = 'sms'
|
|
@cc.set_confirmation_code(true)
|
|
expect(@cc.confirmation_code.size).to eql(4)
|
|
end
|
|
|
|
it "should default the path type to email" do
|
|
communication_channel_model
|
|
expect(@cc.path_type).to eql('email')
|
|
end
|
|
|
|
it "should provide a confirmation url" do
|
|
HostUrl.expects(:protocol).returns('https')
|
|
HostUrl.expects(:context_host).returns('test.canvas.com')
|
|
CanvasSlug.expects(:generate).returns('abc123')
|
|
communication_channel_model
|
|
expect(@cc.confirmation_url).to eql('https://test.canvas.com/register/abc123')
|
|
end
|
|
|
|
it "should only allow email, or sms as path types" do
|
|
communication_channel_model
|
|
@cc.path_type = 'email'; @cc.save
|
|
expect(@cc.path_type).to eql('email')
|
|
|
|
@cc.path_type = 'sms'; @cc.save
|
|
expect(@cc.path_type).to eql('sms')
|
|
|
|
@cc.path_type = 'not valid'; @cc.save
|
|
expect(@cc.path_type).to eql('email')
|
|
end
|
|
|
|
it "should act as list" do
|
|
expect(CommunicationChannel).to be_respond_to(:acts_as_list)
|
|
end
|
|
|
|
it "should scope the list to the user" do
|
|
@u1 = User.create!
|
|
@u2 = User.create!
|
|
expect(@u1).not_to eql(@u2)
|
|
expect(@u1.id).not_to eql(@u2.id)
|
|
@cc1 = @u1.communication_channels.create!(:path => 'jt@instructure.com')
|
|
@cc2 = @u1.communication_channels.create!(:path => 'cody@instructure.com')
|
|
@cc3 = @u2.communication_channels.create!(:path => 'brianp@instructure.com')
|
|
expect(@cc1.user).to eql(@u1)
|
|
expect(@cc2.user).to eql(@u1)
|
|
expect(@cc3.user).to eql(@u2)
|
|
expect(@cc1.user_id).not_to eql(@cc3.user_id)
|
|
expect(@cc2.position).to eql(2)
|
|
@cc2.move_to_top
|
|
@cc2.save
|
|
@cc2.reload
|
|
expect(@cc2.position).to eql(1)
|
|
@cc1.reload
|
|
expect(@cc1.position).to eql(2)
|
|
@cc3.reload
|
|
expect(@cc3.position).to eql(1)
|
|
end
|
|
|
|
context "can_notify?" do
|
|
it "should normally be able to be used" do
|
|
communication_channel_model
|
|
expect(@communication_channel).to be_can_notify
|
|
end
|
|
|
|
it "should not be able to be used if it has a policy to not use it" do
|
|
communication_channel_model
|
|
notification_policy_model(:frequency => "never", :communication_channel => @communication_channel)
|
|
@communication_channel.reload
|
|
expect(@communication_channel).not_to be_can_notify
|
|
end
|
|
end
|
|
|
|
describe "by_email" do
|
|
it "should return matching ccs case-insensitively" do
|
|
@user = User.create!
|
|
@cc = @user.communication_channels.create!(:path => 'user@example.com')
|
|
expect(@user.communication_channels.by_path('USER@EXAMPLE.COM')).to eq [@cc]
|
|
end
|
|
end
|
|
|
|
it "should properly validate the uniqueness of path" do
|
|
@user = User.create!
|
|
@cc = @user.communication_channels.create!(:path => 'user1@example.com')
|
|
# should allow a different address
|
|
@user.communication_channels.create!(:path => 'user2@example.com')
|
|
# should allow a different path_type
|
|
@user.communication_channels.create!(:path => 'user1@example.com', :path_type => 'sms')
|
|
end
|
|
|
|
context "destroy_permanently!" do
|
|
it "does not violate foreign key constraints" do
|
|
communication_channel_model
|
|
notification_policy_model(:frequency => "daily", :communication_channel => @communication_channel)
|
|
delayed_message_model(:notification_policy_id => @notification_policy.id)
|
|
@communication_channel.destroy_permanently!
|
|
end
|
|
end
|
|
|
|
context "notifications" do
|
|
it "should forward the root account to the message" do
|
|
notification = Notification.create!(:name => 'Confirm Email Communication Channel', :category => 'Registration')
|
|
@user = User.create!
|
|
@user.register!
|
|
@cc = @user.communication_channels.create!(:path => 'user1@example.com')
|
|
account = Account.create!
|
|
HostUrl.stubs(:context_host).with(account).returns('someserver.com')
|
|
HostUrl.stubs(:context_host).with(@cc).returns('someserver.com')
|
|
HostUrl.stubs(:context_host).with(nil).returns('default')
|
|
@cc.send_confirmation!(account)
|
|
message = Message.where(:communication_channel_id => @cc, :notification_id => notification).first
|
|
expect(message).not_to be_nil
|
|
expect(message.body).to match /someserver.com/
|
|
end
|
|
end
|
|
|
|
it "should not allow deleting sms channels that are the otp channel" do
|
|
user_with_pseudonym(:active_all => 1)
|
|
@cc = @user.communication_channels.sms.create!(:path => 'bob')
|
|
@cc.confirm!
|
|
@user.otp_communication_channel = @cc
|
|
@user.save!
|
|
@cc.reload
|
|
expect(@cc.destroy).to be_falsey
|
|
expect(@cc.reload).to be_active
|
|
end
|
|
|
|
describe '#last_bounce_summary' do
|
|
it 'gets the diagnostic code' do
|
|
user = User.create!
|
|
cc = user.communication_channels.create!(path: 'path@example.com') do |cc|
|
|
cc.last_bounce_details = {
|
|
'bouncedRecipients' => [
|
|
{
|
|
'diagnosticCode' => 'stuff and things'
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
expect(cc.last_bounce_summary).to eq('stuff and things')
|
|
end
|
|
|
|
it "doesn't fail when there isn't a last bounce" do
|
|
user = User.create!
|
|
cc = user.communication_channels.create!(path: 'path@example.com')
|
|
|
|
expect(cc.last_bounce_details).to be_nil
|
|
expect(cc.last_bounce_summary).to be_nil
|
|
end
|
|
end
|
|
|
|
describe '#last_transient_bounce_summary' do
|
|
it 'gets the diagnostic code' do
|
|
user = User.create!
|
|
cc = user.communication_channels.create!(path: 'path@example.com') do |cc|
|
|
cc.last_transient_bounce_details = {
|
|
'bouncedRecipients' => [
|
|
{
|
|
'diagnosticCode' => 'stuff and things'
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
expect(cc.last_transient_bounce_summary).to eq('stuff and things')
|
|
end
|
|
|
|
it "doesn't fail when there isn't a last transient bounce" do
|
|
user = User.create!
|
|
cc = user.communication_channels.create!(path: 'path@example.com')
|
|
|
|
expect(cc.last_transient_bounce_details).to be_nil
|
|
expect(cc.last_transient_bounce_summary).to be_nil
|
|
end
|
|
end
|
|
|
|
describe "merge candidates" do
|
|
let_once(:user1) { User.create! }
|
|
let_once(:cc1) { user1.communication_channels.create!(:path => 'jt@instructure.com') }
|
|
it "should return users with a matching e-mail address" do
|
|
user2 = User.create!
|
|
cc2 = user2.communication_channels.create!(:path => 'jt@instructure.com')
|
|
cc2.confirm!
|
|
Account.default.pseudonyms.create!(:user => user2, :unique_id => 'user2')
|
|
|
|
expect(cc1.merge_candidates).to eq [user2]
|
|
expect(cc1.has_merge_candidates?).to be_truthy
|
|
end
|
|
|
|
it "should not return users without an active pseudonym" do
|
|
user2 = User.create!
|
|
cc2 = user2.communication_channels.create!(:path => 'jt@instructure.com')
|
|
cc2.confirm!
|
|
|
|
expect(cc1.merge_candidates).to eq []
|
|
expect(cc1.has_merge_candidates?).to be_falsey
|
|
end
|
|
|
|
it "should not return users that match on an unconfirmed cc" do
|
|
user2 = User.create!
|
|
cc2 = user2.communication_channels.create!(:path => 'jt@instructure.com')
|
|
Account.default.pseudonyms.create!(:user => user2, :unique_id => 'user2')
|
|
|
|
expect(cc1.merge_candidates).to eq []
|
|
expect(cc1.has_merge_candidates?).to be_falsey
|
|
end
|
|
|
|
it "should only check one user for boolean result" do
|
|
user2 = User.create!
|
|
cc2 = user2.communication_channels.create!(:path => 'jt@instructure.com')
|
|
cc2.confirm!
|
|
Account.default.pseudonyms.create!(:user => user2, :unique_id => 'user2')
|
|
user3 = User.create!
|
|
cc3 = user3.communication_channels.create!(:path => 'jt@instructure.com')
|
|
cc3.confirm!
|
|
Account.default.pseudonyms.create!(:user => user3, :unique_id => 'user3')
|
|
|
|
User.any_instance.expects(:all_active_pseudonyms).once.returns([true])
|
|
expect(cc1.has_merge_candidates?).to be_truthy
|
|
end
|
|
|
|
it "does not return users for push channels" do
|
|
user2 = User.create!
|
|
cc2 = user2.communication_channels.create!(:path => 'push', :path_type => CommunicationChannel::TYPE_PUSH)
|
|
cc2.confirm!
|
|
Account.default.pseudonyms.create!(:user => user2, :unique_id => 'user2')
|
|
user3 = User.create!
|
|
cc3 = user3.communication_channels.create!(:path => 'push', :path_type => CommunicationChannel::TYPE_PUSH)
|
|
cc3.confirm!
|
|
Account.default.pseudonyms.create!(:user => user3, :unique_id => 'user3')
|
|
|
|
expect(cc1.has_merge_candidates?).to be_falsey
|
|
end
|
|
|
|
describe ".bounce_for_path" do
|
|
it "flags paths with too many bounces" do
|
|
@cc1 = communication_channel_model(path: 'not_as_bouncy@example.edu')
|
|
@cc2 = communication_channel_model(path: 'bouncy@example.edu')
|
|
|
|
%w{bouncy@example.edu Bouncy@example.edu bOuNcY@Example.edu bouncy@example.edu bouncy@example.edu}.each do |path|
|
|
CommunicationChannel.bounce_for_path(
|
|
path: path,
|
|
timestamp: nil,
|
|
details: nil,
|
|
permanent_bounce: true,
|
|
suppression_bounce: false
|
|
)
|
|
end
|
|
|
|
@cc1.reload
|
|
expect(@cc1.bounce_count).to eq 0
|
|
expect(@cc1.bouncing?).to be_falsey
|
|
|
|
@cc2.reload
|
|
expect(@cc2.bounce_count).to eq 5
|
|
expect(@cc2.bouncing?).to be_truthy
|
|
end
|
|
|
|
it "stores the date of the last hard bounce" do
|
|
cc = communication_channel_model(
|
|
path: 'foo@bar.edu',
|
|
last_bounce_at: '2015-01-01T01:01:01.000Z',
|
|
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z',
|
|
last_transient_bounce_at: '2015-04-04T04:04:04.000Z'
|
|
)
|
|
CommunicationChannel.bounce_for_path(
|
|
path: 'foo@bar.edu',
|
|
timestamp: '2015-02-02T02:02:02.000Z',
|
|
details: nil,
|
|
permanent_bounce: true,
|
|
suppression_bounce: false
|
|
)
|
|
|
|
cc.reload
|
|
expect(cc.last_bounce_at).to eq('2015-02-02T02:02:02.000Z')
|
|
expect(cc.last_suppression_bounce_at).to eq('2015-03-03T03:03:03.000Z')
|
|
expect(cc.last_transient_bounce_at).to eq('2015-04-04T04:04:04.000Z')
|
|
end
|
|
|
|
it "stores the date of the last soft bounce bounce" do
|
|
cc = communication_channel_model(
|
|
path: 'foo@bar.edu',
|
|
last_bounce_at: '2015-01-01T01:01:01.000Z',
|
|
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z',
|
|
last_transient_bounce_at: '2015-04-04T04:04:04.000Z'
|
|
)
|
|
CommunicationChannel.bounce_for_path(
|
|
path: 'foo@bar.edu',
|
|
timestamp: '2015-05-05T05:05:05.000Z',
|
|
details: nil,
|
|
permanent_bounce: false,
|
|
suppression_bounce: false
|
|
)
|
|
|
|
cc.reload
|
|
expect(cc.last_bounce_at).to eq('2015-01-01T01:01:01.000Z')
|
|
expect(cc.last_suppression_bounce_at).to eq('2015-03-03T03:03:03.000Z')
|
|
expect(cc.last_transient_bounce_at).to eq('2015-05-05T05:05:05.000Z')
|
|
end
|
|
|
|
it "stores the date of the last suppression bounce" do
|
|
cc = communication_channel_model(
|
|
path: 'foo@bar.edu',
|
|
last_bounce_at: '2015-01-01T01:01:01.000Z',
|
|
last_suppression_bounce_at: '2015-03-03T03:03:03.000Z',
|
|
last_transient_bounce_at: '2015-04-04T04:04:04.000Z'
|
|
)
|
|
CommunicationChannel.bounce_for_path(
|
|
path: 'foo@bar.edu',
|
|
timestamp: '2015-02-02T02:02:02.000Z',
|
|
details: nil,
|
|
permanent_bounce: true,
|
|
suppression_bounce: true
|
|
)
|
|
|
|
cc.reload
|
|
expect(cc.last_bounce_at).to eq('2015-01-01T01:01:01.000Z')
|
|
expect(cc.last_suppression_bounce_at).to eq('2015-02-02T02:02:02.000Z')
|
|
expect(cc.last_transient_bounce_at).to eq('2015-04-04T04:04:04.000Z')
|
|
end
|
|
|
|
it "stores the details of the last hard bounce" do
|
|
cc = communication_channel_model(path: 'foo@bar.edu')
|
|
CommunicationChannel.bounce_for_path(
|
|
path: 'foo@bar.edu',
|
|
timestamp: nil,
|
|
details: {'some' => 'details', 'foo' => 'bar'},
|
|
permanent_bounce: true,
|
|
suppression_bounce: false
|
|
)
|
|
|
|
cc.reload
|
|
expect(cc.last_bounce_details).to eq('some' => 'details', 'foo' => 'bar')
|
|
expect(cc.last_transient_bounce_details).to be_nil
|
|
end
|
|
|
|
it "stores the details of the last soft bounce" do
|
|
cc = communication_channel_model(path: 'foo@bar.edu')
|
|
CommunicationChannel.bounce_for_path(
|
|
path: 'foo@bar.edu',
|
|
timestamp: nil,
|
|
details: {'some' => 'details', 'foo' => 'bar'},
|
|
permanent_bounce: false,
|
|
suppression_bounce: false
|
|
)
|
|
|
|
cc.reload
|
|
expect(cc.last_transient_bounce_details).to eq('some' => 'details', 'foo' => 'bar')
|
|
expect(cc.last_bounce_details).to be_nil
|
|
end
|
|
|
|
it "does not store the details of the last suppression bounce" do
|
|
cc = communication_channel_model(
|
|
path: 'foo@bar.edu',
|
|
last_bounce_details: {'existing' => 'details'}
|
|
)
|
|
CommunicationChannel.bounce_for_path(
|
|
path: 'foo@bar.edu',
|
|
timestamp: nil,
|
|
details: {'some' => 'details', 'foo' => 'bar'},
|
|
permanent_bounce: true,
|
|
suppression_bounce: true
|
|
)
|
|
|
|
cc.reload
|
|
expect(cc.last_bounce_details).to eq('existing' => 'details')
|
|
expect(cc.last_transient_bounce_details).to be_nil
|
|
end
|
|
end
|
|
|
|
context "sharding" do
|
|
specs_require_sharding
|
|
|
|
it "should find a match on another shard" do
|
|
Enrollment.stubs(:cross_shard_invitations?).returns(true)
|
|
@shard1.activate do
|
|
@user2 = User.create!
|
|
cc2 = @user2.communication_channels.create!(:path => 'jt@instructure.com')
|
|
cc2.confirm!
|
|
account = Account.create!
|
|
account.pseudonyms.create!(:user => @user2, :unique_id => 'user2')
|
|
end
|
|
|
|
skip if CommunicationChannel.associated_shards('jt@instructure.com') == [Shard.default]
|
|
|
|
expect(cc1.merge_candidates).to eq [@user2]
|
|
expect(cc1.has_merge_candidates?).to be_truthy
|
|
end
|
|
|
|
it "should search a non-default shard *only*" do
|
|
Enrollment.stubs(:cross_shard_invitations?).returns(false)
|
|
cc1.confirm!
|
|
Account.default.pseudonyms.create!(:user => user1, :unique_id => 'user1')
|
|
|
|
@shard1.activate do
|
|
@user2 = User.create!
|
|
@cc2 = @user2.communication_channels.create!(:path => 'jt@instructure.com')
|
|
@cc2.confirm!
|
|
account = Account.create!
|
|
account.pseudonyms.create!(:user => @user2, :unique_id => 'user2')
|
|
end
|
|
|
|
expect(cc1.merge_candidates).to eq []
|
|
expect(@cc2.merge_candidates).to eq []
|
|
end
|
|
|
|
describe ".bounce_for_path" do
|
|
it "flags paths with too many bounces" do
|
|
@cc1 = communication_channel_model(path: 'not_as_bouncy@example.edu')
|
|
@shard1.activate do
|
|
@cc2 = communication_channel_model(path: 'bouncy@example.edu')
|
|
end
|
|
|
|
skip if CommunicationChannel.associated_shards('bouncy@example.edu') == [Shard.default]
|
|
|
|
@shard2.activate do
|
|
@cc3 = communication_channel_model(path: 'BOUNCY@example.edu')
|
|
end
|
|
|
|
%w{bouncy@example.edu Bouncy@example.edu bOuNcY@Example.edu bouncy@example.edu bouncy@example.edu}.each do |path|
|
|
CommunicationChannel.bounce_for_path(
|
|
path: path,
|
|
timestamp: nil,
|
|
details: nil,
|
|
permanent_bounce: true,
|
|
suppression_bounce: false
|
|
)
|
|
end
|
|
|
|
@cc1.reload
|
|
expect(@cc1.bounce_count).to eq 0
|
|
expect(@cc1.bouncing?).to be_falsey
|
|
|
|
@cc2.reload
|
|
expect(@cc2.bounce_count).to eq 5
|
|
expect(@cc2.bouncing?).to be_truthy
|
|
|
|
@cc3.reload
|
|
expect(@cc3.bounce_count).to eq 5
|
|
expect(@cc3.bouncing?).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|