discourse/spec/requests/export_csv_controller_spec.rb
Gary Pendergast 7fc8d74f3e
FEATURE: Allow admins to export users (#30918)
The GDPR requires all users to be able to export their data, or request an export of their data. This is fine for active users as we have a data export button on user profiles, but suspended users have no way of accessing the data export function, and the workaround for admins to export data for suspended users involves temporarily unsuspending them, then impersonating the user to export the data as them.

Since suspended users no longer have access to their account, we can safely assume that the export request will be coming via a medium outside of Discourse (eg, email). This change is built with this workflow in mind.

This change adds a new "User exports" section to the admin user page, allowing admins to start a new export, and to download the latest export file.
2025-01-24 08:13:25 +11:00

233 lines
8.1 KiB
Ruby

# frozen_string_literal: true
RSpec.describe ExportCsvController do
context "while logged in as normal user" do
fab!(:user)
fab!(:user2) { Fabricate(:user) }
before { sign_in(user) }
describe "#export_entity" do
it "enqueues user archive job" do
post "/export_csv/export_entity.json", params: { entity: "user_archive" }
expect(response.status).to eq(200)
expect(Jobs::ExportUserArchive.jobs.size).to eq(1)
job_data = Jobs::ExportUserArchive.jobs.first["args"].first
expect(job_data["user_id"]).to eq(user.id)
end
it "should not enqueue export job if rate limit is reached" do
UserExport.create(file_name: "user-archive-codinghorror-150116-003249", user_id: user.id)
post "/export_csv/export_entity.json", params: { entity: "user_archive" }
expect(response.status).to eq(422)
expect(Jobs::ExportUserArchive.jobs.size).to eq(0)
end
it "returns 404 when normal user tries to export admin entity" do
post "/export_csv/export_entity.json", params: { entity: "staff_action" }
expect(response.status).to eq(422)
expect(Jobs::ExportCsvFile.jobs.size).to eq(0)
end
it "does not allow a normal user to export another user's archive" do
post "/export_csv/export_entity.json",
params: {
entity: "user_archive",
args: {
export_user_id: user2.id,
},
}
expect(response.status).to eq(422)
expect(Jobs::ExportUserArchive.jobs.size).to eq(0)
end
it "correctly logs the entity export" do
post "/export_csv/export_entity.json", params: { entity: "user_archive" }
log_entry = UserHistory.last
expect(log_entry.action).to eq(UserHistory.actions[:entity_export])
expect(log_entry.acting_user_id).to eq(user.id)
expect(log_entry.subject).to eq("user_archive")
end
end
describe "#latest_user_archive" do
it "returns the latest user archive" do
export = generate_exports(user)
get "/export_csv/latest_user_archive/#{user.id}.json"
expect(response.status).to eq(200)
expect(response.parsed_body["user_export"]["id"]).to eq(export.id)
end
it "returns nothing when the user has no archives" do
get "/export_csv/latest_user_archive/#{user.id}.json"
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(nil)
end
it "does not allow a normal user to view another user's archive" do
generate_exports(user2)
get "/export_csv/latest_user_archive/#{user2.id}.json"
expect(response.status).to eq(403)
end
end
end
context "while logged in as an admin" do
fab!(:user)
fab!(:admin)
before { sign_in(admin) }
describe "#export_entity" do
it "enqueues export job" do
post "/export_csv/export_entity.json", params: { entity: "staff_action" }
expect(response.status).to eq(200)
expect(Jobs::ExportCsvFile.jobs.size).to eq(1)
job_data = Jobs::ExportCsvFile.jobs.first["args"].first
expect(job_data["entity"]).to eq("staff_action")
expect(job_data["user_id"]).to eq(admin.id)
end
it "should not rate limit export for staff" do
UserExport.create(file_name: "screened-email-150116-010145", user_id: admin.id)
post "/export_csv/export_entity.json", params: { entity: "staff_action" }
expect(response.status).to eq(200)
expect(Jobs::ExportCsvFile.jobs.size).to eq(1)
job_data = Jobs::ExportCsvFile.jobs.first["args"].first
expect(job_data["entity"]).to eq("staff_action")
expect(job_data["user_id"]).to eq(admin.id)
end
it "allows user archives for other users" do
post "/export_csv/export_entity.json",
params: {
entity: "user_archive",
args: {
export_user_id: user.id,
},
}
expect(response.status).to eq(200)
expect(Jobs::ExportUserArchive.jobs.size).to eq(1)
job_data = Jobs::ExportUserArchive.jobs.first["args"].first
expect(job_data["user_id"]).to eq(user.id)
end
it "correctly logs the entity export" do
post "/export_csv/export_entity.json", params: { entity: "user_list" }
log_entry = UserHistory.last
expect(log_entry.action).to eq(UserHistory.actions[:entity_export])
expect(log_entry.acting_user_id).to eq(admin.id)
expect(log_entry.subject).to eq("user_list")
end
it "fails requests where the entity is too long" do
post "/export_csv/export_entity.json", params: { entity: "x" * 200 }
expect(response.status).to eq(400)
end
it "fails requests where the name arg is too long" do
post "/export_csv/export_entity.json", params: { entity: "foo", args: { name: "x" * 200 } }
expect(response.status).to eq(400)
end
end
describe "#latest_user_archive" do
it "allows an admin to view another user's archive" do
export = generate_exports(user)
get "/export_csv/latest_user_archive/#{user.id}.json"
expect(response.status).to eq(200)
expect(response.parsed_body["user_export"]["id"]).to eq(export.id)
end
end
end
context "while logged in as a moderator" do
fab!(:user)
fab!(:moderator)
before { sign_in(moderator) }
describe "#export_entity" do
it "does not allow moderators to export user_list" do
post "/export_csv/export_entity.json", params: { entity: "user_list" }
expect(response.status).to eq(422)
end
it "does not allow moderators to export screened_email if they has no permission to view emails" do
SiteSetting.moderators_view_emails = false
post "/export_csv/export_entity.json", params: { entity: "screened_email" }
expect(response.status).to eq(422)
end
it "allows moderator to export screened_email if they has permission to view emails" do
SiteSetting.moderators_view_emails = true
post "/export_csv/export_entity.json", params: { entity: "screened_email" }
expect(response.status).to eq(200)
expect(response.parsed_body["success"]).to eq("OK")
job_data = Jobs::ExportCsvFile.jobs.first["args"].first
expect(job_data["entity"]).to eq("screened_email")
expect(job_data["user_id"]).to eq(moderator.id)
end
it "does not allow moderators to export another user's archive" do
post "/export_csv/export_entity.json",
params: {
entity: "user_archive",
args: {
export_user_id: user.id,
},
}
expect(response.status).to eq(422)
expect(Jobs::ExportUserArchive.jobs.size).to eq(0)
end
it "allows moderator to export other entities" do
post "/export_csv/export_entity.json", params: { entity: "staff_action" }
expect(response.status).to eq(200)
expect(Jobs::ExportCsvFile.jobs.size).to eq(1)
job_data = Jobs::ExportCsvFile.jobs.first["args"].first
expect(job_data["entity"]).to eq("staff_action")
expect(job_data["user_id"]).to eq(moderator.id)
end
end
describe "#latest_user_archive" do
it "does not allow a moderator to view another user's archive" do
generate_exports(user)
get "/export_csv/latest_user_archive/#{user.id}.json"
expect(response.status).to eq(403)
end
end
end
def generate_exports(user)
csv_file_1 = Fabricate(:upload, created_at: 1.day.ago)
topic_1 = Fabricate(:topic, created_at: 1.day.ago)
Fabricate(:post, topic: topic_1)
UserExport.create!(
file_name: "test",
user: user,
upload_id: csv_file_1.id,
topic_id: topic_1.id,
created_at: 1.day.ago,
)
csv_file_2 = Fabricate(:upload, created_at: 12.hours.ago)
topic_2 = Fabricate(:topic, created_at: 12.hours.ago)
UserExport.create!(
file_name: "test2",
user: user,
upload_id: csv_file_2.id,
topic_id: topic_2.id,
created_at: 12.hours.ago,
)
end
end