discourse/spec/components/auth/managed_authenticator_spec.rb
David Taylor cdf4d7156e
DEV: Introduce Auth::Result API for overrides_* (#15378)
This allows authenticators to instruct the Auth::Result to override attributes without using the general site settings. This provides an easy migration path for auth plugins which offer their own "overrides email", "overrides username" or "overrides name" settings. With this new api, they can set `overrides_*` on the result object, and the attribute will be overriden regardless of the general site setting.

ManagedAuthenticator is updated to use this new API. Plugins which consume ManagedAuthenticator will instantly take advantage of this change.
2021-12-23 10:53:17 +00:00

303 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
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) }
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(0)
# 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" })) }
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(0)
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)) }
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(0)
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
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