# 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