mirror of
https://github.com/discourse/discourse.git
synced 2024-12-02 12:43:42 +08:00
9174716737
This method is a huge footgun in production, since it calls
the Redis KEYS command. From the Redis documentation at
https://redis.io/commands/keys/:
> Warning: consider KEYS as a command that should only be used in
production environments with extreme care. It may ruin performance when
it is executed against large databases. This command is intended for
debugging and special operations, such as changing your keyspace layout.
Don't use KEYS in your regular application code.
Since we were only using `delete_prefixed` in specs (now that we
removed the usage in production in 24ec06ff85
)
we can remove this and instead rely on `use_redis_snapshotting` on the
particular tests that need this kind of clearing functionality.
1013 lines
31 KiB
Ruby
1013 lines
31 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe Admin::BackupsController do
|
|
fab!(:admin) { Fabricate(:admin) }
|
|
fab!(:moderator) { Fabricate(:moderator) }
|
|
fab!(:user) { Fabricate(:user) }
|
|
|
|
let(:backup_filename) { "2014-02-10-065935.tar.gz" }
|
|
let(:backup_filename2) { "2014-02-11-065935.tar.gz" }
|
|
|
|
def create_backup_files(*filenames)
|
|
@paths =
|
|
filenames.map do |filename|
|
|
path = backup_path(filename)
|
|
File.open(path, "w") { |f| f.write("test backup") }
|
|
path
|
|
end
|
|
end
|
|
|
|
def backup_path(filename)
|
|
File.join(BackupRestore::LocalBackupStore.base_directory, filename)
|
|
end
|
|
|
|
def map_preloaded
|
|
controller
|
|
.instance_variable_get("@preloaded")
|
|
.map { |key, value| [key, JSON.parse(value)] }
|
|
.to_h
|
|
end
|
|
|
|
before { SiteSetting.backup_location = BackupLocationSiteSetting::LOCAL }
|
|
|
|
after do
|
|
Discourse.redis.flushdb
|
|
|
|
@paths&.each { |path| File.delete(path) if File.exist?(path) }
|
|
@paths = nil
|
|
end
|
|
|
|
describe "#index" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "raises an error when backups are disabled" do
|
|
SiteSetting.enable_backups = false
|
|
get "/admin/backups.json"
|
|
expect(response.status).to eq(403)
|
|
end
|
|
|
|
context "with html format" do
|
|
it "preloads important data" do
|
|
get "/admin/backups.html"
|
|
expect(response.status).to eq(200)
|
|
|
|
preloaded = map_preloaded
|
|
expect(preloaded["operations_status"].symbolize_keys).to eq(
|
|
BackupRestore.operations_status,
|
|
)
|
|
expect(preloaded["logs"].size).to eq(BackupRestore.logs.size)
|
|
end
|
|
end
|
|
|
|
context "with json format" do
|
|
it "returns a list of all the backups" do
|
|
begin
|
|
create_backup_files(backup_filename, backup_filename2)
|
|
|
|
get "/admin/backups.json"
|
|
expect(response.status).to eq(200)
|
|
|
|
filenames = response.parsed_body.map { |backup| backup["filename"] }
|
|
expect(filenames).to include(backup_filename)
|
|
expect(filenames).to include(backup_filename2)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples "backups inaccessible" do
|
|
it "denies access with a 404 response" do
|
|
get "/admin/backups.html"
|
|
|
|
expect(response.status).to eq(404)
|
|
|
|
get "/admin/backups.json"
|
|
|
|
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) }
|
|
|
|
include_examples "backups inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backups inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#status" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "returns the current backups status" do
|
|
get "/admin/backups/status.json"
|
|
expect(response.body).to eq(BackupRestore.operations_status.to_json)
|
|
expect(response.status).to eq(200)
|
|
end
|
|
end
|
|
|
|
shared_examples "status inaccessible" do
|
|
it "denies access with a 404 response" do
|
|
get "/admin/backups/status.json"
|
|
|
|
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) }
|
|
|
|
include_examples "status inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "status inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#create" do
|
|
context "when logged in as an admin" do
|
|
before do
|
|
sign_in(admin)
|
|
BackupRestore.stubs(:backup!)
|
|
end
|
|
|
|
it "starts a backup" do
|
|
BackupRestore.expects(:backup!).with(
|
|
admin.id,
|
|
{ publish_to_message_bus: true, with_uploads: false, client_id: "foo" },
|
|
)
|
|
|
|
post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
context "with rate limiting enabled" do
|
|
before { RateLimiter.enable }
|
|
|
|
use_redis_snapshotting
|
|
|
|
after { RateLimiter.disable }
|
|
|
|
it "is rate limited" do
|
|
post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
|
|
post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
|
|
|
|
expect(response).to have_http_status :too_many_requests
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples "backups creation not allowed" do
|
|
it "prevents backups creation with a 404 response" do
|
|
post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
|
|
|
|
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) }
|
|
|
|
include_examples "backups creation not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backups creation not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#show" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "uses send_file to transmit the backup" do
|
|
begin
|
|
token = EmailBackupToken.set(admin.id)
|
|
create_backup_files(backup_filename)
|
|
|
|
expect do
|
|
get "/admin/backups/#{backup_filename}.json", params: { token: token }
|
|
end.to change {
|
|
UserHistory.where(action: UserHistory.actions[:backup_download]).count
|
|
}.by(1)
|
|
|
|
expect(response.headers["Content-Length"]).to eq("11")
|
|
expect(response.headers["Content-Disposition"]).to match(/attachment; filename/)
|
|
end
|
|
end
|
|
|
|
it "returns 422 when token is bad" do
|
|
begin
|
|
get "/admin/backups/#{backup_filename}.json", params: { token: "bad_value" }
|
|
|
|
expect(response.status).to eq(422)
|
|
expect(response.headers["Content-Disposition"]).not_to match(/attachment; filename/)
|
|
expect(response.body).to include(I18n.t("download_backup_mailer.no_token"))
|
|
end
|
|
end
|
|
|
|
it "returns 404 when the backup does not exist" do
|
|
token = EmailBackupToken.set(admin.id)
|
|
get "/admin/backups/#{backup_filename}.json", params: { token: token }
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
shared_examples "backup inaccessible" do
|
|
it "denies access with a 404 response" do
|
|
begin
|
|
token = EmailBackupToken.set(admin.id)
|
|
create_backup_files(backup_filename)
|
|
|
|
expect do
|
|
get "/admin/backups/#{backup_filename}.json", params: { token: token }
|
|
end.not_to change {
|
|
UserHistory.where(action: UserHistory.actions[:backup_download]).count
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(response.headers["Content-Disposition"]).not_to match(/attachment; filename/)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "backup inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backup inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#destroy" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "removes the backup if found" do
|
|
begin
|
|
path = backup_path(backup_filename)
|
|
create_backup_files(backup_filename)
|
|
expect(File.exist?(path)).to eq(true)
|
|
|
|
expect do delete "/admin/backups/#{backup_filename}.json" end.to change {
|
|
UserHistory.where(action: UserHistory.actions[:backup_destroy]).count
|
|
}.by(1)
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(File.exist?(path)).to eq(false)
|
|
end
|
|
end
|
|
|
|
it "doesn't remove the backup if not found" do
|
|
delete "/admin/backups/#{backup_filename}.json"
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
shared_examples "backup deletion not allowed" do
|
|
it "prevents deletion with a 404 response" do
|
|
begin
|
|
path = backup_path(backup_filename)
|
|
create_backup_files(backup_filename)
|
|
expect(File.exist?(path)).to eq(true)
|
|
|
|
expect do delete "/admin/backups/#{backup_filename}.json" end.not_to change {
|
|
UserHistory.where(action: UserHistory.actions[:backup_destroy]).count
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(File.exist?(path)).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "backup deletion not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backup deletion not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#logs" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "preloads important data" do
|
|
get "/admin/backups/logs.html"
|
|
expect(response.status).to eq(200)
|
|
|
|
preloaded = map_preloaded
|
|
|
|
expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status)
|
|
expect(preloaded["logs"].size).to eq(BackupRestore.logs.size)
|
|
end
|
|
end
|
|
|
|
shared_examples "backup logs inaccessible" do
|
|
it "denies access with a 404 response" do
|
|
get "/admin/backups/logs.html"
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "backup logs inaccessible"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backup logs inaccessible"
|
|
end
|
|
end
|
|
|
|
describe "#restore" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "starts a restore" do
|
|
BackupRestore.expects(:restore!).with(
|
|
admin.id,
|
|
{ filename: backup_filename, publish_to_message_bus: true, client_id: "foo" },
|
|
)
|
|
|
|
post "/admin/backups/#{backup_filename}/restore.json", params: { client_id: "foo" }
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
end
|
|
|
|
shared_examples "backup restoration not allowed" do
|
|
it "prevents restoration with a 404 response" do
|
|
post "/admin/backups/#{backup_filename}/restore.json", params: { client_id: "foo" }
|
|
|
|
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) }
|
|
|
|
include_examples "backup restoration not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backup restoration not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#readonly" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "enables readonly mode" do
|
|
expect(Discourse.readonly_mode?).to eq(false)
|
|
|
|
expect { put "/admin/backups/readonly.json", params: { enable: true } }.to change {
|
|
UserHistory.where(
|
|
action: UserHistory.actions[:change_readonly_mode],
|
|
new_value: "t",
|
|
).count
|
|
}.by(1)
|
|
|
|
expect(Discourse.readonly_mode?).to eq(true)
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "disables readonly mode" do
|
|
Discourse.enable_readonly_mode(Discourse::USER_READONLY_MODE_KEY)
|
|
expect(Discourse.readonly_mode?).to eq(true)
|
|
|
|
expect { put "/admin/backups/readonly.json", params: { enable: false } }.to change {
|
|
UserHistory.where(
|
|
action: UserHistory.actions[:change_readonly_mode],
|
|
new_value: "f",
|
|
).count
|
|
}.by(1)
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(Discourse.readonly_mode?).to eq(false)
|
|
end
|
|
end
|
|
|
|
shared_examples "enabling readonly mode not allowed" do
|
|
it "prevents enabling readonly mode with a 404 response" do
|
|
expect(Discourse.readonly_mode?).to eq(false)
|
|
|
|
expect do put "/admin/backups/readonly.json", params: { enable: true } end.not_to change {
|
|
UserHistory.where(
|
|
action: UserHistory.actions[:change_readonly_mode],
|
|
new_value: "t",
|
|
).count
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
|
expect(Discourse.readonly_mode?).to eq(false)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "enabling readonly mode not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "enabling readonly mode not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#upload_backup_chunk" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
describe "when filename contains invalid characters" do
|
|
it "should raise an error" do
|
|
["灰色.tar.gz", '; echo \'haha\'.tar.gz'].each do |invalid_filename|
|
|
described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true)
|
|
|
|
post "/admin/backups/upload",
|
|
params: {
|
|
resumableFilename: invalid_filename,
|
|
resumableTotalSize: 1,
|
|
resumableIdentifier: "test",
|
|
}
|
|
|
|
expect(response.status).to eq(415)
|
|
expect(response.body).to eq(I18n.t("backup.invalid_filename"))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "when resumableIdentifier is invalid" do
|
|
it "should raise an error" do
|
|
filename = "test_site-0123456789.tar.gz"
|
|
@paths = [backup_path(File.join("tmp", "test", "#{filename}.part1"))]
|
|
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: 1,
|
|
resumableIdentifier: "../test",
|
|
resumableChunkNumber: "1",
|
|
resumableChunkSize: "1",
|
|
resumableCurrentChunkSize: "1",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
end
|
|
|
|
describe "when filename is valid" do
|
|
it "should upload the file successfully" do
|
|
freeze_time
|
|
described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true)
|
|
|
|
filename = "test_Site-0123456789.tar.gz"
|
|
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: 1,
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "1",
|
|
resumableChunkSize: "1",
|
|
resumableCurrentChunkSize: "1",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
expect_job_enqueued(
|
|
job: :backup_chunks_merger,
|
|
args: {
|
|
filename: filename,
|
|
identifier: "test",
|
|
chunks: 1,
|
|
},
|
|
at: 5.seconds.from_now,
|
|
)
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to eq("")
|
|
end
|
|
end
|
|
|
|
describe "completing an upload by enqueuing backup_chunks_merger" do
|
|
let(:filename) { "test_Site-0123456789.tar.gz" }
|
|
|
|
it "works with a single chunk" do
|
|
freeze_time
|
|
described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true)
|
|
|
|
# 2MB file, 2MB chunks = 1x 2MB chunk
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: "2097152",
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "1",
|
|
resumableChunkSize: "2097152",
|
|
resumableCurrentChunkSize: "2097152",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
expect_job_enqueued(
|
|
job: :backup_chunks_merger,
|
|
args: {
|
|
filename: filename,
|
|
identifier: "test",
|
|
chunks: 1,
|
|
},
|
|
at: 5.seconds.from_now,
|
|
)
|
|
end
|
|
|
|
it "works with multiple chunks when the final chunk is chunk_size + remainder" do
|
|
freeze_time
|
|
described_class.any_instance.expects(:has_enough_space_on_disk?).twice.returns(true)
|
|
|
|
# 5MB file, 2MB chunks = 1x 2MB chunk + 1x 3MB chunk with resumable.js
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: "5242880",
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "1",
|
|
resumableChunkSize: "2097152",
|
|
resumableCurrentChunkSize: "2097152",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: "5242880",
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "2",
|
|
resumableChunkSize: "2097152",
|
|
resumableCurrentChunkSize: "3145728",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
expect_job_enqueued(
|
|
job: :backup_chunks_merger,
|
|
args: {
|
|
filename: filename,
|
|
identifier: "test",
|
|
chunks: 2,
|
|
},
|
|
at: 5.seconds.from_now,
|
|
)
|
|
end
|
|
|
|
it "works with multiple chunks when the final chunk is just the remainder" do
|
|
freeze_time
|
|
described_class.any_instance.expects(:has_enough_space_on_disk?).times(3).returns(true)
|
|
|
|
# 5MB file, 2MB chunks = 2x 2MB chunk + 1x 1MB chunk with uppy.js
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: "5242880",
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "1",
|
|
resumableChunkSize: "2097152",
|
|
resumableCurrentChunkSize: "2097152",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: "5242880",
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "2",
|
|
resumableChunkSize: "2097152",
|
|
resumableCurrentChunkSize: "2097152",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: "5242880",
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "3",
|
|
resumableChunkSize: "2097152",
|
|
resumableCurrentChunkSize: "1048576",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
expect_job_enqueued(
|
|
job: :backup_chunks_merger,
|
|
args: {
|
|
filename: filename,
|
|
identifier: "test",
|
|
chunks: 3,
|
|
},
|
|
at: 5.seconds.from_now,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples "uploading backup chunk not allowed" do
|
|
it "prevents uploading of backup chunk with a 404 response" do
|
|
freeze_time
|
|
filename = "test_Site-0123456789.tar.gz"
|
|
|
|
post "/admin/backups/upload.json",
|
|
params: {
|
|
resumableFilename: filename,
|
|
resumableTotalSize: 1,
|
|
resumableIdentifier: "test",
|
|
resumableChunkNumber: "1",
|
|
resumableChunkSize: "1",
|
|
resumableCurrentChunkSize: "1",
|
|
file: fixture_file_upload(Tempfile.new),
|
|
}
|
|
|
|
expect_not_enqueued_with(
|
|
job: :backup_chunks_merger,
|
|
args: {
|
|
filename: filename,
|
|
identifier: "test",
|
|
chunks: 1,
|
|
},
|
|
at: 5.seconds.from_now,
|
|
)
|
|
|
|
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) }
|
|
|
|
include_examples "uploading backup chunk not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "uploading backup chunk not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#check_backup_chunk" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
describe "when resumableIdentifier is invalid" do
|
|
it "should raise an error" do
|
|
get "/admin/backups/upload",
|
|
params: {
|
|
resumableidentifier: "../some_file",
|
|
resumablefilename: "test_site-0123456789.tar.gz",
|
|
resumablechunknumber: "1",
|
|
resumablecurrentchunksize: "1",
|
|
}
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples "checking backup chunk not allowed" do
|
|
it "denies access with a 404 response" do
|
|
get "/admin/backups/upload",
|
|
params: {
|
|
resumableidentifier: "../some_file",
|
|
resumablefilename: "test_site-0123456789.tar.gz",
|
|
resumablechunknumber: "1",
|
|
resumablecurrentchunksize: "1",
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "checking backup chunk not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "checking backup chunk not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#rollback" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "should rollback the restore" do
|
|
BackupRestore.expects(:rollback!)
|
|
|
|
post "/admin/backups/rollback.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "should not allow rollback via a GET request" do
|
|
get "/admin/backups/rollback.json"
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
shared_examples "backup rollback not allowed" do
|
|
it "prevents rollbacks with a 404 response" do
|
|
post "/admin/backups/rollback.json"
|
|
|
|
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) }
|
|
|
|
include_examples "backup rollback not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backup rollback not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#cancel" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "should cancel an backup" do
|
|
BackupRestore.expects(:cancel!)
|
|
|
|
delete "/admin/backups/cancel.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "should not allow cancel via a GET request" do
|
|
get "/admin/backups/cancel.json"
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
shared_examples "backup cancellation not allowed" do
|
|
it "prevents cancellation with a 404 response" do
|
|
delete "/admin/backups/cancel.json"
|
|
|
|
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) }
|
|
|
|
include_examples "backup cancellation not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backup cancellation not allowed"
|
|
end
|
|
end
|
|
|
|
describe "#email" do
|
|
context "when logged in as an admin" do
|
|
before { sign_in(admin) }
|
|
|
|
it "enqueues email job" do
|
|
# might as well test this here if we really want www.example.com
|
|
SiteSetting.force_hostname = "www.example.com"
|
|
|
|
create_backup_files(backup_filename)
|
|
|
|
expect { put "/admin/backups/#{backup_filename}.json" }.to change {
|
|
Jobs::DownloadBackupEmail.jobs.size
|
|
}.by(1)
|
|
|
|
job_args = Jobs::DownloadBackupEmail.jobs.last["args"].first
|
|
expect(job_args["user_id"]).to eq(admin.id)
|
|
expect(job_args["backup_file_path"]).to eq(
|
|
"http://www.example.com/admin/backups/#{backup_filename}",
|
|
)
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "returns 404 when the backup does not exist" do
|
|
put "/admin/backups/#{backup_filename}.json"
|
|
|
|
expect(response).to be_not_found
|
|
end
|
|
end
|
|
|
|
shared_examples "backup emails not allowed" do
|
|
it "prevents sending backup emails with a 404 response" do
|
|
SiteSetting.force_hostname = "www.example.com"
|
|
create_backup_files(backup_filename)
|
|
|
|
expect do put "/admin/backups/#{backup_filename}.json" end.not_to change {
|
|
Jobs::DownloadBackupEmail.jobs.size
|
|
}
|
|
|
|
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) }
|
|
|
|
include_examples "backup emails not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "backup emails not allowed"
|
|
end
|
|
end
|
|
|
|
describe "S3 multipart uploads" do
|
|
let(:upload_type) { "backup" }
|
|
let(:test_bucket_prefix) { "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}" }
|
|
let(:backup_file_exists_response) { { status: 404 } }
|
|
let(:mock_multipart_upload_id) do
|
|
"ibZBv_75gd9r8lH_gqXatLdxMVpAlj6CFTR.OwyF3953YdwbcQnMA2BLGn8Lx12fQNICtMw5KyteFeHw.Sjng--"
|
|
end
|
|
|
|
before do
|
|
setup_s3
|
|
SiteSetting.enable_direct_s3_uploads = true
|
|
SiteSetting.s3_backup_bucket = "s3-backup-bucket"
|
|
SiteSetting.backup_location = BackupLocationSiteSetting::S3
|
|
stub_request(:head, "https://s3-backup-bucket.s3.us-west-1.amazonaws.com/").to_return(
|
|
status: 200,
|
|
body: "",
|
|
headers: {
|
|
},
|
|
)
|
|
stub_request(
|
|
:head,
|
|
"https://s3-backup-bucket.s3.us-west-1.amazonaws.com/default/test.tar.gz",
|
|
).to_return(backup_file_exists_response)
|
|
end
|
|
|
|
shared_examples "multipart uploads not allowed" do
|
|
it "prevents multipart uploads with a 404 response" do
|
|
post "/admin/backups/create-multipart.json",
|
|
params: {
|
|
file_name: "test.tar.gz",
|
|
upload_type: upload_type,
|
|
file_size: 4098,
|
|
}
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
context "when logged in as a moderator" do
|
|
before { sign_in(moderator) }
|
|
|
|
include_examples "multipart uploads not allowed"
|
|
end
|
|
|
|
context "when logged in as a non-staff user" do
|
|
before { sign_in(user) }
|
|
|
|
include_examples "multipart uploads not allowed"
|
|
end
|
|
|
|
context "when the user is admin" do
|
|
before { sign_in(admin) }
|
|
|
|
def stub_create_multipart_backup_request
|
|
BackupRestore::S3BackupStore
|
|
.any_instance
|
|
.stubs(:temporary_upload_path)
|
|
.returns(
|
|
"temp/default/#{test_bucket_prefix}/28fccf8259bbe75b873a2bd2564b778c/2u98j832nx93272x947823.gz",
|
|
)
|
|
create_multipart_result = <<~XML
|
|
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n
|
|
<InitiateMultipartUploadResult>
|
|
<Bucket>s3-backup-bucket</Bucket>
|
|
<Key>temp/default/#{test_bucket_prefix}/28fccf8259bbe75b873a2bd2564b778c/2u98j832nx93272x947823.gz</Key>
|
|
<UploadId>#{mock_multipart_upload_id}</UploadId>
|
|
</InitiateMultipartUploadResult>
|
|
XML
|
|
stub_request(
|
|
:post,
|
|
"https://s3-backup-bucket.s3.us-west-1.amazonaws.com/temp/default/#{test_bucket_prefix}/28fccf8259bbe75b873a2bd2564b778c/2u98j832nx93272x947823.gz?uploads",
|
|
).to_return(status: 200, body: create_multipart_result)
|
|
end
|
|
|
|
it "creates the multipart upload" do
|
|
stub_create_multipart_backup_request
|
|
post "/admin/backups/create-multipart.json",
|
|
params: {
|
|
file_name: "test.tar.gz",
|
|
upload_type: upload_type,
|
|
file_size: 4098,
|
|
}
|
|
expect(response.status).to eq(200)
|
|
result = response.parsed_body
|
|
|
|
external_upload_stub =
|
|
ExternalUploadStub.where(
|
|
unique_identifier: result["unique_identifier"],
|
|
original_filename: "test.tar.gz",
|
|
created_by: admin,
|
|
upload_type: upload_type,
|
|
key: result["key"],
|
|
multipart: true,
|
|
)
|
|
expect(external_upload_stub.exists?).to eq(true)
|
|
end
|
|
|
|
context "when backup of same filename already exists" do
|
|
let(:backup_file_exists_response) { { status: 200, body: "" } }
|
|
|
|
it "throws an error" do
|
|
post "/admin/backups/create-multipart.json",
|
|
params: {
|
|
file_name: "test.tar.gz",
|
|
upload_type: upload_type,
|
|
file_size: 4098,
|
|
}
|
|
expect(response.status).to eq(422)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("backup.file_exists"))
|
|
end
|
|
end
|
|
|
|
context "when filename is invalid" do
|
|
it "throws an error" do
|
|
post "/admin/backups/create-multipart.json",
|
|
params: {
|
|
file_name: "blah $$##.tar.gz",
|
|
upload_type: upload_type,
|
|
file_size: 4098,
|
|
}
|
|
expect(response.status).to eq(422)
|
|
expect(response.parsed_body["errors"]).to include(I18n.t("backup.invalid_filename"))
|
|
end
|
|
end
|
|
|
|
context "when extension is invalid" do
|
|
it "throws an error" do
|
|
post "/admin/backups/create-multipart.json",
|
|
params: {
|
|
file_name: "test.png",
|
|
upload_type: upload_type,
|
|
file_size: 4098,
|
|
}
|
|
expect(response.status).to eq(422)
|
|
expect(response.parsed_body["errors"]).to include(
|
|
I18n.t("backup.backup_file_should_be_tar_gz"),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|