# frozen_string_literal: true
RSpec.describe UploadRecovery do
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
let(:upload) do
UploadCreator.new(file_from_fixtures("smallest.png"), "logo.png").create_for(user.id)
end
let(:upload2) do
UploadCreator.new(file_from_fixtures("small.pdf", "pdf"), "some.pdf").create_for(user.id)
end
let(:post) do
Fabricate(:post, raw: "![logo.png](#{upload.short_url})", user: user).tap(&:link_post_uploads)
end
let(:upload_recovery) { UploadRecovery.new }
before do
SiteSetting.authorized_extensions = "png|pdf"
Jobs.run_immediately!
end
after do
[upload, upload2].each do |u|
next if u
public_path = "#{Discourse.store.public_dir}#{u.url}"
[public_path, public_path.sub("uploads", "uploads/tombstone")].each do |path|
File.delete(path) if File.exist?(path)
end
end
end
describe "#recover" do
describe "when given an invalid sha1" do
it "does nothing" do
upload_recovery.expects(:recover_from_local).never
post.update!(raw: "![logo.png](upload://#{"a" * 28}.png)")
upload_recovery.recover
post.update!(
raw: "test",
)
upload_recovery.recover
end
end
it "accepts a custom ActiveRecord relation" do
post.update!(updated_at: 2.days.ago)
upload.destroy!
upload_recovery.expects(:recover_from_local).never
upload_recovery.recover(Post.where("updated_at >= ?", 1.day.ago))
end
describe "for a missing attachment" do
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
some.pdf
blank
SQL
it "recovers the attachment" do
expect do upload2.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
File.read(file_from_fixtures("small.pdf", "pdf")),
)
end
end
it "recovers uploads and attachments" do
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
File.read(file_from_fixtures("smallest.png")),
)
end
describe "S3 store" do
before do
setup_s3
stub_s3_store
end
it "recovers the upload" do
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
original_key = Discourse.store.get_path_for_upload(upload)
tombstone_key = original_key.sub("original", "tombstone/original")
tombstone_copy = stub
tombstone_copy.expects(:key).returns(tombstone_key)
Discourse.store.s3_helper.expects(:list).with("original").returns([])
Discourse
.store
.s3_helper
.expects(:list)
.with("#{FileStore::S3Store::TOMBSTONE_PREFIX}original")
.returns([tombstone_copy])
Discourse
.store
.s3_helper
.expects(:copy)
.with(tombstone_key, original_key, options: { acl: "public-read" })
FileHelper.expects(:download).returns(file_from_fixtures("smallest.png"))
stub_request(:get, upload.url).to_return(body: file_from_fixtures("smallest.png"))
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
end
describe "when the upload exists but its file is missing" do
it "recovers the file" do
upload.verification_status = Upload.verification_statuses[:invalid_etag]
upload.save!
original_key = Discourse.store.get_path_for_upload(upload)
tombstone_key = original_key.sub("original", "tombstone/original")
tombstone_copy = stub
tombstone_copy.expects(:key).returns(tombstone_key)
Discourse.store.s3_helper.expects(:list).with("original").returns([])
Discourse
.store
.s3_helper
.expects(:list)
.with("#{FileStore::S3Store::TOMBSTONE_PREFIX}original")
.returns([tombstone_copy])
Discourse
.store
.s3_helper
.expects(:copy)
.with(tombstone_key, original_key, options: { acl: "public-read" })
expect do upload_recovery.recover end.to_not change {
[post.reload.uploads.count, Upload.count]
}
end
it "does not create a duplicate upload when secure uploads are enabled" do
SiteSetting.secure_uploads = true
upload.verification_status = Upload.verification_statuses[:invalid_etag]
upload.save!
original_key = Discourse.store.get_path_for_upload(upload)
tombstone_key = original_key.sub("original", "tombstone/original")
tombstone_copy = stub
tombstone_copy.expects(:key).returns(tombstone_key)
Discourse.store.s3_helper.expects(:list).with("original").returns([])
Discourse
.store
.s3_helper
.expects(:list)
.with("#{FileStore::S3Store::TOMBSTONE_PREFIX}original")
.returns([tombstone_copy])
Discourse
.store
.s3_helper
.expects(:copy)
.with(tombstone_key, original_key, options: { acl: "public-read" })
expect do upload_recovery.recover end.to_not change {
[post.reload.uploads.count, Upload.count]
}
end
end
end
describe "image tag" do
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
SQL
it "recovers the upload" do
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
File.read(file_from_fixtures("smallest.png")),
)
end
end
describe "image markdown" do
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
![image](#{upload.url})
SQL
it "recovers the upload" do
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
File.read(file_from_fixtures("smallest.png")),
)
end
end
describe "bbcode" do
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
[img]#{upload.url}[/img]
SQL
it "recovers the upload" do
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
File.read(file_from_fixtures("smallest.png")),
)
end
end
end
end