discourse/spec/support/fake_s3.rb

191 lines
4.3 KiB
Ruby

# frozen_string_literal: true
require "aws-sdk-s3"
class FakeS3
attr_reader :s3_client
def self.create
s3 = self.new
s3.stub_bucket(SiteSetting.s3_upload_bucket) if SiteSetting.s3_upload_bucket.present?
if SiteSetting.s3_backup_bucket.present?
s3.stub_bucket(
File.join(SiteSetting.s3_backup_bucket, RailsMultisite::ConnectionManagement.current_db),
)
end
s3.stub_s3_helper
s3
end
def initialize
@buckets = {}
@operations = []
@s3_client = Aws::S3::Client.new(stub_responses: true, region: SiteSetting.s3_region)
stub_methods
end
def bucket(bucket_name)
bucket_name, _prefix = bucket_name.split("/", 2)
@buckets[bucket_name]
end
def stub_bucket(full_bucket_name)
bucket_name, _prefix = full_bucket_name.split("/", 2)
s3_helper =
S3Helper.new(
full_bucket_name,
(
if Rails.configuration.multisite
FileStore::S3Store.new.multisite_tombstone_prefix
else
FileStore::S3Store::TOMBSTONE_PREFIX
end
),
client: @s3_client,
)
@buckets[bucket_name] = FakeS3Bucket.new(full_bucket_name, s3_helper)
end
def stub_s3_helper
@buckets.each do |bucket_name, bucket|
S3Helper
.stubs(:new)
.with { |b| b == bucket_name || b == bucket.name }
.returns(bucket.s3_helper)
end
end
def operation_called?(name)
@operations.any? do |operation|
operation[:name] == name && (block_given? ? yield(operation) : true)
end
end
private
def find_bucket(params)
bucket(params[:bucket])
end
def find_object(params)
bucket = find_bucket(params)
bucket&.find_object(params[:key])
end
def log_operation(context)
@operations << { name: context.operation_name, params: context.params.dup }
end
def calculate_etag(context)
# simple, reproducible ETag calculation
Digest::MD5.hexdigest(context.params.to_json)
end
def stub_methods
@s3_client.stub_responses(
:head_object,
->(context) do
log_operation(context)
if object = find_object(context.params)
{
content_length: object[:size],
last_modified: object[:last_modified],
metadata: object[:metadata],
}
else
{ status_code: 404, headers: {}, body: "" }
end
end,
)
@s3_client.stub_responses(
:get_object,
->(context) do
log_operation(context)
if object = find_object(context.params)
{ content_length: object[:size], body: "" }
else
{ status_code: 404, headers: {}, body: "" }
end
end,
)
@s3_client.stub_responses(
:delete_object,
->(context) do
log_operation(context)
find_bucket(context.params)&.delete_object(context.params[:key])
nil
end,
)
@s3_client.stub_responses(
:copy_object,
->(context) do
log_operation(context)
source_bucket_name, source_key = context.params[:copy_source].split("/", 2)
copy_source = { bucket: source_bucket_name, key: source_key }
if context.params[:metadata_directive] == "REPLACE"
attribute_overrides = context.params.except(:copy_source, :metadata_directive)
else
attribute_overrides = context.params.slice(:key, :bucket)
end
new_object = find_object(copy_source).dup.merge(attribute_overrides)
find_bucket(new_object).put_object(new_object)
{ copy_object_result: { etag: calculate_etag(context) } }
end,
)
@s3_client.stub_responses(
:create_multipart_upload,
->(context) do
log_operation(context)
find_bucket(context.params).put_object(context.params)
{ upload_id: SecureRandom.hex }
end,
)
@s3_client.stub_responses(
:put_object,
->(context) do
log_operation(context)
find_bucket(context.params).put_object(context.params)
{ etag: calculate_etag(context) }
end,
)
end
end
class FakeS3Bucket
attr_reader :name, :s3_helper
def initialize(bucket_name, s3_helper)
@name = bucket_name
@s3_helper = s3_helper
@objects = {}
end
def put_object(obj)
@objects[obj[:key]] = obj
end
def delete_object(key)
@objects.delete(key)
end
def find_object(key)
@objects[key]
end
end