# frozen_string_literal: true RSpec.describe DiscourseConnect do before do @discourse_connect_url = "http://example.com/discourse_sso" @discourse_connect_secret = "shjkfdhsfkjh" SiteSetting.discourse_connect_url = @discourse_connect_url SiteSetting.enable_discourse_connect = true SiteSetting.discourse_connect_secret = @discourse_connect_secret SiteSetting.reserved_usernames = "" Jobs.run_immediately! end def make_sso sso = DiscourseConnectBase.new sso.sso_url = "http://meta.discorse.org/topics/111" sso.sso_secret = "supersecret" sso.nonce = "testing" sso.email = "some@email.com" sso.username = "sam" sso.name = "sam saffron" sso.external_id = "100" sso.avatar_url = "https://cdn.discourse.org/user_avatar.png" sso.avatar_force_update = false sso.bio = "about" sso.admin = false sso.moderator = false sso.suppress_welcome_message = false sso.require_activation = false sso.title = "user title" sso.custom_fields["a"] = "Aa" sso.custom_fields["b.b"] = "B.b" sso.website = "https://www.discourse.org/" sso.location = "Home" sso end def new_discourse_sso DiscourseConnect.new(secure_session: secure_session) end def test_parsed(parsed, sso) expect(parsed.nonce).to eq sso.nonce expect(parsed.email).to eq sso.email expect(parsed.username).to eq sso.username expect(parsed.name).to eq sso.name expect(parsed.external_id).to eq sso.external_id expect(parsed.avatar_url).to eq sso.avatar_url expect(parsed.avatar_force_update).to eq sso.avatar_force_update expect(parsed.bio).to eq sso.bio expect(parsed.admin).to eq sso.admin expect(parsed.moderator).to eq sso.moderator expect(parsed.suppress_welcome_message).to eq sso.suppress_welcome_message expect(parsed.require_activation).to eq false expect(parsed.title).to eq sso.title expect(parsed.custom_fields["a"]).to eq "Aa" expect(parsed.custom_fields["b.b"]).to eq "B.b" expect(parsed.website).to eq sso.website expect(parsed.location).to eq sso.location end it "can do round trip parsing correctly" do sso = DiscourseConnectBase.new sso.sso_secret = "test" sso.name = "sam saffron" sso.username = "sam" sso.email = "sam@sam.com" sso = DiscourseConnectBase.parse(sso.payload, "test") expect(sso.name).to eq "sam saffron" expect(sso.username).to eq "sam" expect(sso.email).to eq "sam@sam.com" end let(:ip_address) { "127.0.0.1" } let(:secure_session) { SecureSession.new("abc") } it "bans bad external id" do sso = new_discourse_sso sso.username = "test" sso.name = "" sso.email = "test@test.com" sso.suppress_welcome_message = true sso.external_id = " " expect do sso.lookup_or_create_user(ip_address) end.to raise_error( DiscourseConnect::BlankExternalId, ) sso.external_id = nil expect do sso.lookup_or_create_user(ip_address) end.to raise_error( DiscourseConnect::BlankExternalId, ) # going for slight duplication here so our intent is crystal clear %w[none nil Blank null].each do |word| sso.external_id = word expect do sso.lookup_or_create_user(ip_address) end.to raise_error( DiscourseConnect::BannedExternalId, ) end end it "can lookup or create user when name is blank" do sso = new_discourse_sso sso.username = "test" sso.name = "" sso.email = "test@test.com" sso.external_id = "A" sso.suppress_welcome_message = true user = sso.lookup_or_create_user(ip_address) expect(user.persisted?).to eq(true) end it "unstaged users" do SiteSetting.auth_overrides_name = true email = "staged@user.com" Fabricate(:user, staged: true, email: email) sso = new_discourse_sso sso.username = "staged" sso.name = "Bob O'Bob" sso.email = email sso.external_id = "B" user = sso.lookup_or_create_user(ip_address) user.reload expect(user).to_not be_nil expect(user.staged).to be(false) expect(user.name).to eq("Bob O'Bob") end describe "reviewables" do let(:sso) do new_discourse_sso.tap do |sso| sso.username = "staged" sso.name = "Bob O'Bob" sso.email = "bob@obob.com" sso.external_id = "B" end end it "doesn't create reviewables if we aren't approving users" do user = sso.lookup_or_create_user(ip_address) reviewable = ReviewableUser.find_by(target: user) expect(reviewable).to be_blank end it "creates reviewables if needed" do SiteSetting.must_approve_users = true user = sso.lookup_or_create_user(ip_address) reviewable = ReviewableUser.find_by(target: user) expect(reviewable).to be_present expect(reviewable).to be_pending end end it "can set admin and moderator" do admin_group = Group[:admins] mod_group = Group[:moderators] staff_group = Group[:staff] sso = new_discourse_sso sso.username = "misteradmin" sso.name = "Bob Admin" sso.email = "admin@admin.com" sso.external_id = "id" sso.admin = true sso.moderator = true sso.suppress_welcome_message = true user = sso.lookup_or_create_user(ip_address) staff_group.reload expect(mod_group.users.where("users.id = ?", user.id).exists?).to eq(true) expect(staff_group.users.where("users.id = ?", user.id).exists?).to eq(true) expect(admin_group.users.where("users.id = ?", user.id).exists?).to eq(true) end it "can force a list of groups with the groups attribute" do user = Fabricate(:user) group1 = Fabricate(:group, name: "group1") group2 = Fabricate(:group, name: "group2") sso = new_discourse_sso sso.username = "bobsky" sso.name = "Bob" sso.email = user.email sso.external_id = "A" sso.groups = "#{group2.name.capitalize},group4,badname,trust_level_4" sso.lookup_or_create_user(ip_address) SiteSetting.discourse_connect_overrides_groups = true group1.reload expect(group1.usernames).to eq("") expect(group2.usernames).to eq("") group1.add(user) group1.save! sso.lookup_or_create_user(ip_address) expect(group1.usernames).to eq("") expect(group2.usernames).to eq(user.username) sso.groups = "badname,trust_level_4" sso.lookup_or_create_user(ip_address) expect(group1.usernames).to eq("") expect(group2.usernames).to eq("") group1.add(user) group1.save! group2.add(user) group2.save! sso.groups = "#{group1.name},badname,trust_level_4" sso.lookup_or_create_user(ip_address) expect(group1.usernames).to eq(user.username) expect(group2.usernames).to eq("") end it "can specify groups" do user = Fabricate(:user) add_group1 = Fabricate(:group, name: "group1") add_group2 = Fabricate(:group, name: "group2") existing_group = Fabricate(:group, name: "group3") add_group4 = Fabricate(:group, name: "GROUP4") existing_group2 = Fabricate(:group, name: "GRoup5") [existing_group, existing_group2].each do |g| g.add(user) g.save! end add_group1.add(user) existing_group.save! sso = new_discourse_sso sso.username = "bobsky" sso.name = "Bob" sso.email = user.email sso.external_id = "A" sso.add_groups = "#{add_group1.name},#{add_group2.name.capitalize},group4,badname" sso.remove_groups = "#{existing_group.name},#{existing_group2.name.downcase},badname" sso.lookup_or_create_user(ip_address) existing_group.reload expect(existing_group.usernames).to eq("") existing_group2.reload expect(existing_group2.usernames).to eq("") add_group1.reload expect(add_group1.usernames).to eq(user.username) add_group2.reload expect(add_group2.usernames).to eq(user.username) add_group4.reload expect(add_group4.usernames).to eq(user.username) end it "creates group logs when users are added to groups" do user = Fabricate(:user) group = Fabricate(:group, name: "group1") sso = new_discourse_sso sso.username = "bobsky" sso.name = "Bob" sso.email = user.email sso.external_id = "A" sso.add_groups = "#{group.name},badname" GroupHistory.destroy_all sso.lookup_or_create_user(ip_address) expect(group.reload.usernames).to eq(user.username) expect( GroupHistory.exists?( target_user_id: user.id, acting_user: Discourse.system_user.id, action: GroupHistory.actions[:add_user_to_group], ), ).to eq(true) end it "creates group logs when users are removed from groups" do user = Fabricate(:user) group = Fabricate(:group, name: "group1") group.add(user) sso = new_discourse_sso sso.username = "bobsky" sso.name = "Bob" sso.email = user.email sso.external_id = "A" sso.remove_groups = "#{group.name},badname" GroupHistory.destroy_all sso.lookup_or_create_user(ip_address) expect(group.reload.usernames).to be_blank expect( GroupHistory.exists?( target_user_id: user.id, acting_user: Discourse.system_user.id, action: GroupHistory.actions[:remove_user_from_group], ), ).to eq(true) end it "creates group logs when users are added to groups when discourse_connect_overrides_groups setting is true" do SiteSetting.discourse_connect_overrides_groups = true user = Fabricate(:user) group = Fabricate(:group, name: "group1") sso = new_discourse_sso sso.username = "bobsky" sso.name = "Bob" sso.email = user.email sso.external_id = "A" sso.groups = "#{group.name},badname" GroupHistory.destroy_all sso.lookup_or_create_user(ip_address) expect(group.reload.usernames).to eq(user.username) expect( GroupHistory.exists?( target_user_id: user.id, acting_user: Discourse.system_user.id, action: GroupHistory.actions[:add_user_to_group], ), ).to eq(true) end it "creates group logs when users are removed from groups when discourse_connect_overrides_groups setting is true" do SiteSetting.discourse_connect_overrides_groups = true user = Fabricate(:user) group = Fabricate(:group, name: "group1") group.add(user) sso = new_discourse_sso sso.username = "bobsky" sso.name = "Bob" sso.email = user.email sso.external_id = "A" sso.groups = "badname" GroupHistory.destroy_all sso.lookup_or_create_user(ip_address) expect(group.reload.usernames).to be_blank expect( GroupHistory.exists?( target_user_id: user.id, acting_user: Discourse.system_user.id, action: GroupHistory.actions[:remove_user_from_group], ), ).to eq(true) end it "behaves properly when auth_overrides_username is set but username is missing or blank" do SiteSetting.auth_overrides_username = true sso = new_discourse_sso sso.username = "testuser" sso.name = "test user" sso.email = "test@test.com" sso.external_id = "100" sso.bio = "This **is** the bio" sso.suppress_welcome_message = true # create the original user user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "testuser" # remove username from payload sso.username = nil user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "testuser" # set username in payload to blank sso.username = "" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "testuser" end it "can override name / email / username" do admin = Fabricate(:admin) SiteSetting.email_editable = false SiteSetting.auth_overrides_name = true SiteSetting.auth_overrides_email = true SiteSetting.auth_overrides_username = true sso = new_discourse_sso sso.username = "bob%the$admin" sso.name = "Bob Admin" sso.email = admin.email sso.external_id = "A" sso.lookup_or_create_user(ip_address) admin.reload expect(admin.name).to eq "Bob Admin" expect(admin.username).to eq "bob_the_admin" expect(admin.email).to eq admin.email sso.email = "TEST@bob.com" sso.name = "Louis C.K." sso.lookup_or_create_user(ip_address) admin.reload expect(admin.email).to eq("test@bob.com") expect(admin.username).to eq "bob_the_admin" expect(admin.name).to eq "Louis C.K." end it "can override username properly when only the case changes" do SiteSetting.auth_overrides_username = true sso = new_discourse_sso sso.username = "testuser" sso.name = "test user" sso.email = "test@test.com" sso.external_id = "100" sso.bio = "This **is** the bio" sso.suppress_welcome_message = true # create the original user user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "testuser" # change the username case sso.username = "TestUser" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "TestUser" end it "do not override username when a new username after fixing is the same" do SiteSetting.auth_overrides_username = true sso = new_discourse_sso sso.username = "testuser" sso.name = "test user" sso.email = "test@test.com" sso.external_id = "100" # create the original user user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "testuser" # change the username case sso.username = "testuserგამარჯობა" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "testuser" end it "should preserve username when several users login with the same username" do SiteSetting.auth_overrides_username = true # if several users have username "bill" on the external site, # they will have usernames bill, bill1, bill2 etc in Discourse: Fabricate(:user, username: "bill") Fabricate(:user, username: "bill1") Fabricate(:user, username: "bill2") Fabricate(:user, username: "bill4") # the number should be preserved during subsequent logins # bill3 should remain bill3 sso = new_discourse_sso sso.username = "bill3" sso.email = "test@test.com" sso.external_id = "100" sso.lookup_or_create_user(ip_address) sso.username = "bill" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "bill3" end it "uses name if it's present in payload" do sso = new_discourse_sso sso.external_id = "100" name = "John" sso.name = name sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.name).to eq name end it "uses username for name suggestions if name isn't present in payload" do sso = new_discourse_sso sso.external_id = "100" sso.name = "" sso.username = "user_john" sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.name).to eq "User John" end it "uses username for username suggestions if it's present in payload" do sso = new_discourse_sso sso.external_id = "100" username = "user_john" sso.username = username sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq username end it "uses name for username suggestions if username isn't present in payload" do sso = new_discourse_sso sso.external_id = "100" sso.username = "" sso.name = "John Smith" sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "John_Smith" end it "uses name for username suggestions if username consists entirely of disallowed characters" do SiteSetting.unicode_usernames = false sso = new_discourse_sso sso.external_id = "100" sso.username = "Πλάτων" sso.name = "Plato" sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq sso.name end it "stops using name as a source for username suggestions when disabled" do SiteSetting.use_name_for_username_suggestions = false sso = new_discourse_sso sso.external_id = "100" sso.username = nil sso.name = "John Smith" sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "user" end it "doesn't use email as a source for username suggestions by default" do sso = new_discourse_sso sso.external_id = "100" # set username and name to nil, so they cannot be used as a source for suggestions sso.username = nil sso.name = nil sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq I18n.t("fallback_username") end it "uses email as a source for username suggestions if enabled" do SiteSetting.use_email_for_username_and_name_suggestions = true sso = new_discourse_sso sso.external_id = "100" # set username and name to nil, so they cannot be used as a source for suggestions sso.username = nil sso.name = nil sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "mail" end it "doesn't use email as a source for name suggestions by default" do sso = new_discourse_sso sso.external_id = "100" # set username and name to nil, so they cannot be used as a source for suggestions sso.username = nil sso.name = nil sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.name).to eq "" end it "uses email as a source for name suggestions if enabled" do SiteSetting.use_email_for_username_and_name_suggestions = true sso = new_discourse_sso sso.external_id = "100" # set username and name to nil, so they cannot be used as a source for suggestions sso.username = nil sso.name = nil sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.name).to eq "Mail" end it "uses email for username suggestions if username and name consist entirely of disallowed characters" do SiteSetting.use_email_for_username_and_name_suggestions = true SiteSetting.unicode_usernames = false sso = new_discourse_sso sso.external_id = "100" sso.username = "Πλάτων" sso.name = "Πλάτων" sso.email = "mail@mail.com" user = sso.lookup_or_create_user(ip_address) expect(user.username).to eq "mail" end it "can override username with a number at the end to a simpler username without a number" do SiteSetting.auth_overrides_username = true user = Fabricate(:user) sso = new_discourse_sso sso.external_id = "A" sso.email = user.email username_with_number = "bob1" username_without_number = "bob" sso.username = username_with_number sso.lookup_or_create_user(ip_address) user.reload expect(user.username).to eq username_with_number sso.username = username_without_number sso.lookup_or_create_user(ip_address) user.reload expect(user.username).to eq username_without_number end it "can override username after min_username_length was made smaller" do SiteSetting.auth_overrides_username = true user = Fabricate(:user) sso = new_discourse_sso sso.external_id = "A" sso.email = user.email long_username = "bob" short_username = "bo" SiteSetting.min_username_length = 3 sso.username = long_username sso.lookup_or_create_user(ip_address) user.reload expect(user.username).to eq long_username SiteSetting.min_username_length = 2 sso.username = short_username sso.lookup_or_create_user(ip_address) user.reload expect(user.username).to eq short_username end it "can fill in data on way back" do sso = make_sso url, payload = sso.to_url.split("?") expect(url).to eq sso.sso_url parsed = DiscourseConnectBase.parse(payload, "supersecret") test_parsed(parsed, sso) end it "handles sso_url with query params" do sso = make_sso sso.sso_url = "http://tcdev7.wpengine.com/?action=showlogin" expect(sso.to_url.split("?").size).to eq 2 url, payload = sso.to_url.split("?") expect(url).to eq "http://tcdev7.wpengine.com/" parsed = DiscourseConnectBase.parse(payload, "supersecret") test_parsed(parsed, sso) end it "validates nonce" do _, payload = DiscourseConnect.generate_url(secure_session: secure_session).split("?") sso = DiscourseConnect.parse(payload, secure_session: secure_session) expect(sso.nonce_valid?).to eq true other_session_sso = DiscourseConnect.parse(payload, secure_session: SecureSession.new("differentsession")) expect(other_session_sso.nonce_valid?).to eq false sso.expire_nonce! expect(sso.nonce_valid?).to eq false end it "allows disabling CSRF protection" do SiteSetting.discourse_connect_csrf_protection = false _, payload = DiscourseConnect.generate_url(secure_session: secure_session).split("?") sso = DiscourseConnect.parse(payload, secure_session: secure_session) expect(sso.nonce_valid?).to eq true other_session_sso = DiscourseConnect.parse(payload, secure_session: SecureSession.new("differentsession")) expect(other_session_sso.nonce_valid?).to eq true sso.expire_nonce! expect(sso.nonce_valid?).to eq false end it "generates a correct sso url" do url, payload = DiscourseConnect.generate_url(secure_session: secure_session).split("?") expect(url).to eq @discourse_connect_url sso = DiscourseConnect.parse(payload, secure_session: secure_session) expect(sso.nonce).to_not be_nil end describe "nonce error" do it "generates correct error message when nonce has already been used" do _, payload = DiscourseConnect.generate_url(secure_session: secure_session).split("?") sso = DiscourseConnect.parse(payload, secure_session: secure_session) expect(sso.nonce_valid?).to eq true sso.expire_nonce! expect(sso.nonce_error).to eq("Nonce has already been used") end it "generates correct error message when nonce is expired" do _, payload = DiscourseConnect.generate_url(secure_session: secure_session).split("?") sso = DiscourseConnect.parse(payload, secure_session: secure_session) expect(sso.nonce_valid?).to eq true Discourse.cache.delete(sso.used_nonce_key) expect(sso.nonce_error).to eq( "Nonce is incorrect, was generated in a different browser session, or has expired", ) end it "generates correct error message when nonce is expired, and csrf protection disabled" do SiteSetting.discourse_connect_csrf_protection = false _, payload = DiscourseConnect.generate_url(secure_session: secure_session).split("?") sso = DiscourseConnect.parse(payload, secure_session: secure_session) expect(sso.nonce_valid?).to eq true Discourse.cache.delete(sso.used_nonce_key) expect(sso.nonce_error).to eq("Nonce is incorrect, or has expired") end end describe "user locale" do it "sets default user locale if specified" do SiteSetting.allow_user_locale = true sso = new_discourse_sso sso.username = "test" sso.name = "test" sso.email = "test@test.com" sso.external_id = "123" sso.locale = "es" user = sso.lookup_or_create_user(ip_address) expect(user.locale).to eq("es") user.update_column(:locale, "he") user = sso.lookup_or_create_user(ip_address) expect(user.locale).to eq("he") sso.locale_force_update = true user = sso.lookup_or_create_user(ip_address) expect(user.locale).to eq("es") sso.locale = "fake" user = sso.lookup_or_create_user(ip_address) expect(user.locale).to eq("es") end end describe "trusting emails" do let(:sso) do sso = new_discourse_sso sso.username = "test" sso.name = "test" sso.email = "test@example.com" sso.external_id = "A" sso.suppress_welcome_message = true sso end it "activates users by default" do user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(true) end it "does not activate user when asked not to" do sso.require_activation = true user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(false) user.activate sso.external_id = "B" expect do sso.lookup_or_create_user(ip_address) end.to raise_error( ActiveRecord::RecordInvalid, ) end it "does not deactivate user if email provided is capitalized" do SiteSetting.email_editable = false SiteSetting.auth_overrides_email = true sso.require_activation = true user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(false) user.update_columns(active: true) user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(true) sso.email = "Test@example.com" user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(true) end it "deactivates accounts that have updated email address" do SiteSetting.email_editable = false SiteSetting.auth_overrides_email = true sso.require_activation = true user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(false) old_email = user.email user.update_columns(active: true) user = sso.lookup_or_create_user(ip_address) expect(user.active).to eq(true) user.primary_email.update_columns(email: "xXx@themovie.com") user = sso.lookup_or_create_user(ip_address) expect(user.email).to eq(old_email) expect(user.active).to eq(false) end end describe "welcome emails" do let(:sso) do sso = new_discourse_sso sso.username = "test" sso.name = "test" sso.email = "test@example.com" sso.external_id = "A" sso end it "sends a welcome email by default" do User.any_instance.expects(:enqueue_welcome_message).once _user = sso.lookup_or_create_user(ip_address) end it "suppresses the welcome email when asked to" do User.any_instance.expects(:enqueue_welcome_message).never sso.suppress_welcome_message = true _user = sso.lookup_or_create_user(ip_address) end end describe "setting title for a user" do let(:sso) do sso = new_discourse_sso sso.username = "test" sso.name = "test" sso.email = "test@test.com" sso.external_id = "100" sso.title = "The User's Title" sso end it "sets title correctly" do user = sso.lookup_or_create_user(ip_address) expect(user.title).to eq(sso.title) sso.title = "farmer" user = sso.lookup_or_create_user(ip_address) expect(user.title).to eq("farmer") sso.title = nil user = sso.lookup_or_create_user(ip_address) expect(user.title).to eq("farmer") end end describe "setting bio for a user" do let(:sso) do sso = new_discourse_sso sso.username = "test" sso.name = "test" sso.email = "test@test.com" sso.external_id = "100" sso.bio = "This **is** the bio" sso.suppress_welcome_message = true sso end it "can set bio if supplied on new users or users with empty bio" do # new account user = sso.lookup_or_create_user(ip_address) expect(user.user_profile.bio_cooked).to match_html("

This is the bio

") # no override by default sso.bio = "new profile" user = sso.lookup_or_create_user(ip_address) expect(user.user_profile.bio_cooked).to match_html("

This is the bio

") # yes override for blank user.user_profile.update!(bio_raw: "") user = sso.lookup_or_create_user(ip_address) expect(user.user_profile.bio_cooked).to match_html("

new profile

") # yes override if site setting sso.bio = "new profile 2" SiteSetting.discourse_connect_overrides_bio = true user = sso.lookup_or_create_user(ip_address) expect(user.user_profile.bio_cooked).to match_html("

new profile 2