373 lines
14 KiB
Ruby
373 lines
14 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe Auth::ManagedAuthenticator do
|
|
let(:authenticator) {
|
|
Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
end.new
|
|
}
|
|
|
|
let(:hash) {
|
|
OmniAuth::AuthHash.new(
|
|
provider: "myauth",
|
|
uid: "1234",
|
|
info: {
|
|
name: "Best Display Name",
|
|
email: "awesome@example.com",
|
|
nickname: "IAmGroot"
|
|
},
|
|
credentials: {
|
|
token: "supersecrettoken"
|
|
},
|
|
extra: {
|
|
raw_info: {
|
|
randominfo: "some info"
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
let(:create_hash) {
|
|
OmniAuth::AuthHash.new(
|
|
provider: "myauth",
|
|
uid: "1234"
|
|
)
|
|
}
|
|
|
|
def create_auth_result(attrs)
|
|
auth_result = Auth::Result.new
|
|
attrs.each { |k, v| auth_result.send("#{k}=", v) }
|
|
auth_result
|
|
end
|
|
|
|
describe 'after_authenticate' do
|
|
it 'can match account from an existing association' do
|
|
user = Fabricate(:user)
|
|
associated = UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", last_used: 1.year.ago)
|
|
result = authenticator.after_authenticate(hash)
|
|
|
|
expect(result.user.id).to eq(user.id)
|
|
associated.reload
|
|
expect(associated.last_used).to be >= 1.day.ago
|
|
expect(associated.info["name"]).to eq("Best Display Name")
|
|
expect(associated.info["email"]).to eq("awesome@example.com")
|
|
expect(associated.credentials["token"]).to eq("supersecrettoken")
|
|
expect(associated.extra["raw_info"]["randominfo"]).to eq("some info")
|
|
end
|
|
|
|
it 'only sets email valid for present strings' do
|
|
# (Twitter sometimes sends empty email strings)
|
|
result = authenticator.after_authenticate(create_hash.merge(info: { email: "email@example.com" }))
|
|
expect(result.email_valid).to eq(true)
|
|
|
|
result = authenticator.after_authenticate(create_hash.merge(info: { email: "" }))
|
|
expect(result.email_valid).to be_falsey
|
|
|
|
result = authenticator.after_authenticate(create_hash.merge(info: { email: nil }))
|
|
expect(result.email_valid).to be_falsey
|
|
end
|
|
|
|
describe 'connecting to another user account' do
|
|
fab!(:user1) { Fabricate(:user) }
|
|
fab!(:user2) { Fabricate(:user) }
|
|
before { UserAssociatedAccount.create!(user: user1, provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it 'works by default' do
|
|
result = authenticator.after_authenticate(hash, existing_account: user2)
|
|
expect(result.user.id).to eq(user2.id)
|
|
expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false)
|
|
expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true)
|
|
end
|
|
|
|
it 'still works if another user has a matching email' do
|
|
Fabricate(:user, email: hash.dig(:info, :email))
|
|
result = authenticator.after_authenticate(hash, existing_account: user2)
|
|
expect(result.user.id).to eq(user2.id)
|
|
expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false)
|
|
expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true)
|
|
end
|
|
|
|
it 'does not work when disabled' do
|
|
authenticator = Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
def can_connect_existing_user?
|
|
false
|
|
end
|
|
end.new
|
|
result = authenticator.after_authenticate(hash, existing_account: user2)
|
|
expect(result.user.id).to eq(user1.id)
|
|
expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(true)
|
|
expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe 'match by email' do
|
|
it 'downcases the email address from the authprovider' do
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { email: "HELLO@example.com" }))
|
|
expect(result.email).to eq('hello@example.com')
|
|
end
|
|
|
|
it 'works normally' do
|
|
user = Fabricate(:user)
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { email: user.email }))
|
|
expect(result.user.id).to eq(user.id)
|
|
expect(UserAssociatedAccount.find_by(provider_name: 'myauth', provider_uid: "1234").user_id).to eq(user.id)
|
|
end
|
|
|
|
it 'works if there is already an association with the target account' do
|
|
user = Fabricate(:user, email: "awesome@example.com")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user.id).to eq(user.id)
|
|
end
|
|
|
|
it 'does not match if match_by_email is false' do
|
|
authenticator = Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
def match_by_email
|
|
false
|
|
end
|
|
end.new
|
|
user = Fabricate(:user, email: "awesome@example.com")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user).to eq(nil)
|
|
end
|
|
end
|
|
|
|
context 'when no matching user' do
|
|
it 'returns the correct information' do
|
|
expect {
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user).to eq(nil)
|
|
expect(result.username).to eq("IAmGroot")
|
|
expect(result.email).to eq("awesome@example.com")
|
|
}.to change { UserAssociatedAccount.count }.by(1)
|
|
expect(UserAssociatedAccount.last.user).to eq(nil)
|
|
expect(UserAssociatedAccount.last.info["nickname"]).to eq("IAmGroot")
|
|
end
|
|
|
|
it 'works if there is already an association with the target account' do
|
|
user = Fabricate(:user, email: "awesome@example.com")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user.id).to eq(user.id)
|
|
end
|
|
|
|
it 'works if there is no email' do
|
|
expect {
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { email: nil }))
|
|
expect(result.user).to eq(nil)
|
|
expect(result.username).to eq("IAmGroot")
|
|
expect(result.email).to eq(nil)
|
|
}.to change { UserAssociatedAccount.count }.by(1)
|
|
expect(UserAssociatedAccount.last.user).to eq(nil)
|
|
expect(UserAssociatedAccount.last.info["nickname"]).to eq("IAmGroot")
|
|
end
|
|
|
|
it 'will ignore name when equal to email' do
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { name: hash.info.email }))
|
|
expect(result.email).to eq(hash.info.email)
|
|
expect(result.name).to eq(nil)
|
|
end
|
|
end
|
|
|
|
describe "avatar on update" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:associated) { UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "schedules the job upon update correctly" do
|
|
# No image supplied, do not schedule
|
|
expect { result = authenticator.after_authenticate(hash) }
|
|
.not_to change { Jobs::DownloadAvatarFromUrl.jobs.count }
|
|
|
|
# Image supplied, schedule
|
|
expect { result = authenticator.after_authenticate(hash.deep_merge(info: { image: "https://some.domain/image.jpg" })) }
|
|
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1)
|
|
|
|
# User already has profile picture, don't schedule
|
|
user.user_avatar = Fabricate(:user_avatar, custom_upload: Fabricate(:upload))
|
|
user.save!
|
|
expect { result = authenticator.after_authenticate(hash.deep_merge(info: { image: "https://some.domain/image.jpg" })) }
|
|
.not_to change { Jobs::DownloadAvatarFromUrl.jobs.count }
|
|
end
|
|
end
|
|
|
|
describe "profile on update" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:associated) { UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "updates the user's location and bio, unless already set" do
|
|
{ description: :bio_raw, location: :location }.each do |auth_hash_key, profile_key|
|
|
user.user_profile.update(profile_key => "Initial Value")
|
|
# No value supplied, do not overwrite
|
|
expect { result = authenticator.after_authenticate(hash) }
|
|
.not_to change { user.user_profile.reload; user.user_profile[profile_key] }
|
|
|
|
# Value supplied, still do not overwrite
|
|
expect { result = authenticator.after_authenticate(hash.deep_merge(info: { auth_hash_key => "New Value" })) }
|
|
.not_to change { user.user_profile.reload; user.user_profile[profile_key] }
|
|
|
|
# User has not set a value, so overwrite
|
|
user.user_profile.update(profile_key => "")
|
|
authenticator.after_authenticate(hash.deep_merge(info: { auth_hash_key => "New Value" }))
|
|
user.user_profile.reload
|
|
expect(user.user_profile[profile_key]).to eq("New Value")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "avatar on create" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "doesn't schedule with no image" do
|
|
expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) }
|
|
.not_to change { Jobs::DownloadAvatarFromUrl.jobs.count }
|
|
end
|
|
|
|
it "schedules with image" do
|
|
association.info["image"] = "https://some.domain/image.jpg"
|
|
association.save!
|
|
expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) }
|
|
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1)
|
|
end
|
|
end
|
|
|
|
describe "profile on create" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "doesn't explode without profile" do
|
|
authenticator.after_create_account(user, create_auth_result(extra_data: create_hash))
|
|
end
|
|
|
|
it "works with profile" do
|
|
association.info["location"] = "DiscourseVille"
|
|
association.info["description"] = "Online forum expert"
|
|
association.save!
|
|
authenticator.after_create_account(user, create_auth_result(extra_data: create_hash))
|
|
expect(user.user_profile.bio_raw).to eq("Online forum expert")
|
|
expect(user.user_profile.location).to eq("DiscourseVille")
|
|
end
|
|
end
|
|
|
|
describe 'match by username' do
|
|
let(:user_match_authenticator) {
|
|
Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
def match_by_email
|
|
false
|
|
end
|
|
def match_by_username
|
|
true
|
|
end
|
|
end.new
|
|
}
|
|
|
|
it 'works normally' do
|
|
SiteSetting.username_change_period = 0
|
|
user = Fabricate(:user)
|
|
result = user_match_authenticator.after_authenticate(hash.deep_merge(info: { nickname: user.username }))
|
|
expect(result.user.id).to eq(user.id)
|
|
expect(UserAssociatedAccount.find_by(provider_name: 'myauth', provider_uid: "1234").user_id).to eq(user.id)
|
|
end
|
|
|
|
it 'works if there is already an association with the target account' do
|
|
SiteSetting.username_change_period = 0
|
|
user = Fabricate(:user, username: "IAmGroot")
|
|
result = user_match_authenticator.after_authenticate(hash)
|
|
expect(result.user.id).to eq(user.id)
|
|
end
|
|
|
|
it 'works if the username is different case' do
|
|
SiteSetting.username_change_period = 0
|
|
user = Fabricate(:user, username: "IAMGROOT")
|
|
result = user_match_authenticator.after_authenticate(hash)
|
|
expect(result.user.id).to eq(user.id)
|
|
end
|
|
|
|
it 'does not match if username_change_period isn\'t 0' do
|
|
SiteSetting.username_change_period = 3
|
|
user = Fabricate(:user, username: "IAmGroot")
|
|
result = user_match_authenticator.after_authenticate(hash)
|
|
expect(result.user).to eq(nil)
|
|
end
|
|
|
|
it 'does not match if default match_by_username not overriden' do
|
|
SiteSetting.username_change_period = 0
|
|
authenticator = Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
end.new
|
|
user = Fabricate(:user, username: "IAmGroot")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user).to eq(nil)
|
|
end
|
|
|
|
it 'does not match if match_by_username is false' do
|
|
SiteSetting.username_change_period = 0
|
|
authenticator = Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
def match_by_username
|
|
false
|
|
end
|
|
end.new
|
|
user = Fabricate(:user, username: "IAmGroot")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user).to eq(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'description_for_user' do
|
|
fab!(:user) { Fabricate(:user) }
|
|
|
|
it 'returns empty string if no entry for user' do
|
|
expect(authenticator.description_for_user(user)).to eq("")
|
|
end
|
|
|
|
it 'returns correct information' do
|
|
association = UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", info: { nickname: "somenickname", email: "test@domain.tld", name: "bestname" })
|
|
expect(authenticator.description_for_user(user)).to eq('test@domain.tld')
|
|
association.update(info: { nickname: "somenickname", name: "bestname" })
|
|
expect(authenticator.description_for_user(user)).to eq('somenickname')
|
|
association.update(info: { nickname: "bestname" })
|
|
expect(authenticator.description_for_user(user)).to eq('bestname')
|
|
association.update(info: {})
|
|
expect(authenticator.description_for_user(user)).to eq(I18n.t("associated_accounts.connected"))
|
|
end
|
|
end
|
|
|
|
describe 'revoke' do
|
|
fab!(:user) { Fabricate(:user) }
|
|
|
|
it 'raises exception if no entry for user' do
|
|
expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound)
|
|
end
|
|
|
|
context "with valid record" do
|
|
before do
|
|
UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", info: { name: "somename" })
|
|
end
|
|
|
|
it 'revokes correctly' do
|
|
expect(authenticator.description_for_user(user)).to eq("somename")
|
|
expect(authenticator.can_revoke?).to eq(true)
|
|
expect(authenticator.revoke(user)).to eq(true)
|
|
|
|
expect(authenticator.description_for_user(user)).to eq("")
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|