0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 15:55:38 +08:00
discourse/spec/requests/admin/groups_controller_spec.rb
Régis Hanol ac8d3b0515
UX: Normalize automatic membership email domains (#41434)
Previously, entering an automatic membership email domain that included
an `@` (such as `@acme.io` or a pasted email or URL) warned the admin
and blocked the save until they hand-edited it down to a bare domain.

This change quietly normalizes those entries to the bare domain —
stripping any scheme, path, port, `@` prefix, casing, and surrounding
whitespace — so common paste mistakes just work, while the server still
rejects genuinely invalid domains on save.

Follow up to #41233
2026-07-03 17:22:18 +02:00

570 lines
18 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe Admin::GroupsController do
fab!(:admin)
fab!(:moderator)
fab!(:user)
fab!(:group)
describe "#create" do
let(:group_params) do
{
group: {
name: "testing",
usernames: [admin.username, user.username].join(","),
owner_usernames: [user.username].join(","),
allow_membership_requests: true,
membership_request_template: "Testing",
members_visibility_level: Group.visibility_levels[:staff],
},
}
end
context "when logged in as an admin" do
let(:group) { Group.last }
before { sign_in(admin) }
it "creates a group with the specified attributes" do
post "/admin/groups.json", params: group_params
expect(response).to have_http_status(:ok)
expect(group).to have_attributes(
name: "testing",
allow_membership_requests: true,
membership_request_template: "Testing",
members_visibility_level: Group.visibility_levels[:staff],
)
expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user)
expect(group.users).to contain_exactly(admin, user)
end
context "with custom_fields" do
before do
plugin = Plugin::Instance.new
plugin.register_editable_group_custom_field :test
end
after { DiscoursePluginRegistry.reset! }
it "only updates allowed user fields" do
params = group_params
params[:group].merge!(custom_fields: { test: :hello1, test2: :hello2 })
post "/admin/groups.json", params: params
group = Group.last
expect(response.status).to eq(200)
expect(group.custom_fields["test"]).to eq("hello1")
expect(group.custom_fields["test2"]).to be_blank
end
it "is secure when there are no registered editable fields" do
DiscoursePluginRegistry.reset!
params = group_params
params[:group].merge!(custom_fields: { test: :hello1, test2: :hello2 })
post "/admin/groups.json", params: params
group = Group.last
expect(response.status).to eq(200)
expect(group.custom_fields["test"]).to be_blank
expect(group.custom_fields["test2"]).to be_blank
end
end
context "with Group.plugin_permitted_params" do
after { DiscoursePluginRegistry.reset! }
it "filter non-permitted params" do
params = group_params
params[:group].merge!(allow_unknown_sender_topic_replies: true)
post "/admin/groups.json", params: params
expect(Group.last.allow_unknown_sender_topic_replies).to eq(false)
end
it "allows plugin to allow custom params" do
params = group_params
params[:group].merge!(allow_unknown_sender_topic_replies: true)
plugin = Plugin::Instance.new
plugin.register_group_param :allow_unknown_sender_topic_replies
post "/admin/groups.json", params: params
expect(Group.last.allow_unknown_sender_topic_replies).to eq(true)
end
end
context "with automatic membership email domains" do
it "normalizes a domain that includes '@' and creates the group" do
post "/admin/groups.json",
params: {
group: {
name: "harness-staff",
automatic_membership_email_domains: "@harness.io",
},
}
expect(response.status).to eq(200)
expect(Group.last.automatic_membership_email_domains).to eq("harness.io")
end
it "surfaces a clear validation error for a genuinely invalid domain" do
post "/admin/groups.json",
params: {
group: {
name: "harness-staff",
automatic_membership_email_domains: "not a domain",
},
}
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include(
I18n.t("groups.errors.invalid_domain", domain: "not a domain"),
)
expect(response.parsed_body["failed"]).to be_nil
end
it "creates the group when the domains are valid" do
post "/admin/groups.json",
params: {
group: {
name: "harness-staff",
automatic_membership_email_domains: "harness.io",
},
}
expect(response.status).to eq(200)
expect(Group.last.automatic_membership_email_domains).to eq("harness.io")
end
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
context "with moderators_manage_groups enabled" do
before { SiteSetting.moderators_manage_groups = true }
it "creates group" do
expect do post "/admin/groups.json", params: group_params end.to change {
Group.count
}.by(1)
expect(response.status).to eq(200)
group = Group.last
expect(group.name).to eq("testing")
expect(group.users).to contain_exactly(admin, user)
expect(group.allow_membership_requests).to eq(true)
expect(group.membership_request_template).to eq("Testing")
expect(group.members_visibility_level).to eq(Group.visibility_levels[:staff])
end
end
context "with moderators_manage_groups disabled" do
before { SiteSetting.moderators_manage_groups = false }
it "prevents creation with a 403 response" do
expect do post "/admin/groups.json", params: group_params end.to_not change {
Group.count
}
expect(response.status).to eq(403)
expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access"))
end
end
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
it "prevents creation with a 404 response" do
expect do post "/admin/groups.json", params: group_params end.to_not change { Group.count }
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
end
describe "#remove_owner" do
let(:user2) { Fabricate(:user) }
let(:user3) { Fabricate(:user) }
context "when logged in as an admin" do
before { sign_in(admin) }
it "should work" do
group.add_owner(user)
delete "/admin/groups/#{group.id}/owners.json", params: { user_id: user.id }
expect(response.status).to eq(200)
expect(group.group_users.where(owner: true)).to eq([])
end
it "should work with multiple users" do
group.add_owner(user)
group.add_owner(user3)
delete "/admin/groups/#{group.id}/owners.json",
params: {
group: {
usernames: "#{user.username},#{user2.username},#{user3.username}",
},
}
expect(response.status).to eq(200)
expect(group.group_users.where(owner: true)).to eq([])
end
it "returns not-found error when there is no group" do
group.destroy!
delete "/admin/groups/#{group.id}/owners.json", params: { user_id: user.id }
expect(response.status).to eq(404)
end
it "does not allow removing owners from an automatic group" do
group.update!(automatic: true)
delete "/admin/groups/#{group.id}/owners.json", params: { user_id: user.id }
expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"])
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
context "with moderators_manage_groups enabled" do
before { SiteSetting.moderators_manage_groups = true }
it "removes owner" do
group.add_owner(user)
delete "/admin/groups/#{group.id}/owners.json", params: { user_id: user.id }
expect(response.status).to eq(200)
expect(group.group_users.where(owner: true)).to eq([])
end
end
context "with moderators_manage_groups disabled" do
before { SiteSetting.moderators_manage_groups = false }
it "prevents owner removal with a 403 response" do
group.add_owner(user)
delete "/admin/groups/#{group.id}/owners.json", params: { user_id: user.id }
expect(response.status).to eq(403)
expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access"))
expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user)
end
end
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
it "prevents owner removal with a 404 response" do
group.add_owner(user)
delete "/admin/groups/#{group.id}/owners.json", params: { user_id: user.id }
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user)
end
end
end
describe "#set_primary" do
let(:user2) { Fabricate(:user) }
let(:user3) { Fabricate(:user) }
context "when logged in as an admin" do
before { sign_in(admin) }
it "sets with multiple users" do
user2.update!(primary_group_id: group.id)
put "/admin/groups/#{group.id}/primary.json",
params: {
group: {
usernames: "#{user.username},#{user2.username},#{user3.username}",
},
primary: "true",
}
expect(response.status).to eq(200)
expect(User.where(primary_group_id: group.id).size).to eq(3)
end
it "unsets with multiple users" do
user.update!(primary_group_id: group.id)
user3.update!(primary_group_id: group.id)
put "/admin/groups/#{group.id}/primary.json",
params: {
group: {
usernames: "#{user.username},#{user2.username},#{user3.username}",
},
primary: "false",
}
expect(response.status).to eq(200)
expect(User.where(primary_group_id: group.id).size).to eq(0)
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
context "with moderators_manage_groups enabled" do
before { SiteSetting.moderators_manage_groups = true }
it "sets multiple primary users" do
user2.update!(primary_group_id: group.id)
put "/admin/groups/#{group.id}/primary.json",
params: {
group: {
usernames: "#{user.username},#{user2.username},#{user3.username}",
},
primary: "true",
}
expect(response.status).to eq(200)
expect(User.where(primary_group_id: group.id).size).to eq(3)
end
end
context "with moderators_manage_groups disabled" do
before { SiteSetting.moderators_manage_groups = false }
it "prevents setting of primary group with a 403 response" do
user2.update!(primary_group_id: group.id)
put "/admin/groups/#{group.id}/primary.json",
params: {
group: {
usernames: "#{user.username},#{user2.username},#{user3.username}",
},
primary: "true",
}
expect(response.status).to eq(403)
expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access"))
expect(User.where(primary_group_id: group.id).size).to eq(1)
end
end
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
it "prevents setting of primary user with a 404 response" do
user2.update!(primary_group_id: group.id)
put "/admin/groups/#{group.id}/primary.json",
params: {
group: {
usernames: "#{user.username},#{user2.username},#{user3.username}",
},
primary: "true",
}
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
expect(User.where(primary_group_id: group.id).size).to eq(1)
end
end
end
describe "#destroy" do
context "when logged in as an admin" do
before { sign_in(admin) }
it "should return the right response for an invalid group_id" do
max_id = Group.maximum(:id).to_i
delete "/admin/groups/#{max_id + 1}.json"
expect(response.status).to eq(404)
end
it "logs when a group is destroyed" do
delete "/admin/groups/#{group.id}.json"
history = UserHistory.where(acting_user: admin).last
expect(history).to be_present
expect(history.details).to include("name: #{group.name}")
expect(history.details).to include("id: #{group.id}")
end
it "logs the grant_trust_level attribute" do
trust_level = TrustLevel[4]
group.update!(grant_trust_level: trust_level)
delete "/admin/groups/#{group.id}.json"
history = UserHistory.where(acting_user: admin).last
expect(history).to be_present
expect(history.details).to include("grant_trust_level: #{trust_level}")
expect(history.details).to include("name: #{group.name}")
end
context "when group is automatic" do
it "returns the right response" do
group.update!(automatic: true)
delete "/admin/groups/#{group.id}.json"
expect(response.status).to eq(422)
expect(Group.find(group.id)).to eq(group)
end
end
context "when group is not automatic" do
it "returns the right response" do
delete "/admin/groups/#{group.id}.json"
expect(response.status).to eq(200)
expect(Group.find_by(id: group.id)).to eq(nil)
end
end
end
shared_examples "group deletion not allowed" do
it "prevents deletion with a 404 response" do
expect do delete "/admin/groups/#{group.id}.json" end.not_to change { Group.count }
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
context "with moderators_manage_groups enabled" do
before { SiteSetting.moderators_manage_groups = true }
include_examples "group deletion not allowed"
end
context "with moderators_manage_groups disabled" do
before { SiteSetting.moderators_manage_groups = false }
include_examples "group deletion not allowed"
end
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
include_examples "group deletion not allowed"
end
end
describe "#automatic_membership_count" do
def count_domains(domains)
put "/admin/groups/automatic_membership_count.json",
params: {
automatic_membership_email_domains: domains,
id: group.id,
}
end
context "when logged in as admin" do
before { sign_in(admin) }
it "returns count of users whose emails match the domain" do
Fabricate(:user, email: "user1@somedomain.org")
Fabricate(:user, email: "user1@somedomain.com")
Fabricate(:user, email: "user1@notsomedomain.com")
count_domains("somedomain.org|somedomain.com")
expect(response.status).to eq(200)
expect(response.parsed_body["user_count"]).to eq(2)
end
it "returns a null count for a long list of domains" do
count_domains(1.upto(11).map { |n| "domain#{n}.com" }.join("|"))
expect(response.status).to eq(200)
expect(response.parsed_body["user_count"]).to be_nil
end
it "ignores invalid domains when counting the matches" do
Fabricate(:user, email: "user1@somedomain.org")
count_domains("somedomain.org|not a domain")
expect(response.status).to eq(200)
expect(response.parsed_body["user_count"]).to eq(1)
end
it "normalizes domains that include '@' before counting" do
Fabricate(:user, email: "user1@somedomain.org")
count_domains("@somedomain.org")
expect(response.status).to eq(200)
expect(response.parsed_body["user_count"]).to eq(1)
end
end
shared_examples "automatic membership count inaccessible" do
it "denies access with a 404 response" do
count_domains("somedomain.org|somedomain.com")
expect(response.status).to eq(404)
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
end
end
context "when logged in as a moderator" do
before { sign_in(moderator) }
context "with moderators_manage_groups enabled" do
before { SiteSetting.moderators_manage_groups = true }
it "returns count of users whose emails match the domain" do
Fabricate(:user, email: "user1@somedomain.org")
count_domains("somedomain.org")
expect(response.status).to eq(200)
expect(response.parsed_body["user_count"]).to eq(1)
end
end
context "with moderators_manage_groups disabled" do
before { SiteSetting.moderators_manage_groups = false }
it "denies access with a 403 response" do
count_domains("somedomain.org")
expect(response.status).to eq(403)
end
end
end
context "when logged in as a non-staff user" do
before { sign_in(user) }
include_examples "automatic membership count inaccessible"
end
end
end