discourse/spec/lib/auth/managed_authenticator_spec.rb

475 lines
15 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Auth::ManagedAuthenticator do
let(:authenticator) do
Class
.new(described_class) do
def name
"myauth"
end
def primary_email_verified?(auth_token)
auth_token[:info][:email_verified]
end
end
.new
end
let(:hash) do
OmniAuth::AuthHash.new(
provider: "myauth",
uid: "1234",
info: {
name: "Best Display Name",
email: "awesome@example.com",
nickname: "IAmGroot",
email_verified: true,
},
credentials: {
token: "supersecrettoken",
},
extra: {
raw_info: {
randominfo: "some info",
},
},
)
end
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", email_verified: true }),
)
expect(result.email_valid).to eq(true)
result =
authenticator.after_authenticate(
create_hash.merge(info: { email: "", email_verified: true }),
)
expect(result.email_valid).to be_falsey
result =
authenticator.after_authenticate(
create_hash.merge(info: { email: nil, email_verified: true }),
)
expect(result.email_valid).to be_falsey
end
it "does not set email valid if email_verified is false" do
result =
authenticator.after_authenticate(
create_hash.merge(info: { email: "email@example.com", email_verified: false }),
)
expect(result.email_valid).to eq(false)
end
describe "connecting to another user account" do
fab!(:user1) { Fabricate(:user) }
fab!(:user2) { Fabricate(:user) }
before do
UserAssociatedAccount.create!(user: user1, provider_name: "myauth", provider_uid: "1234")
end
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)
let!(:associated) do
UserAssociatedAccount.create!(user: user, provider_name: "myauth", provider_uid: "1234")
end
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)
let!(:associated) do
UserAssociatedAccount.create!(user: user, provider_name: "myauth", provider_uid: "1234")
end
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)
let!(:association) do
UserAssociatedAccount.create!(provider_name: "myauth", provider_uid: "1234")
end
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)
let!(:association) do
UserAssociatedAccount.create!(provider_name: "myauth", provider_uid: "1234")
end
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) do
Class
.new(described_class) do
def name
"myauth"
end
def match_by_email
false
end
def match_by_username
true
end
end
.new
end
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 overridden" 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)
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)
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