mongoid_direct_s3_upload 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +246 -0
- data/Rakefile +31 -0
- data/app/assets/javascripts/s3_relay.coffee +118 -0
- data/app/assets/stylesheets/s3_relay.css +31 -0
- data/app/controllers/s3_relay/uploads_controller.rb +71 -0
- data/app/helpers/s3_relay/uploads_helper.rb +24 -0
- data/app/models/s3_relay/upload.rb +73 -0
- data/config/routes.rb +5 -0
- data/lib/s3_relay.rb +4 -0
- data/lib/s3_relay/base.rb +35 -0
- data/lib/s3_relay/engine.rb +16 -0
- data/lib/s3_relay/model.rb +51 -0
- data/lib/s3_relay/private_url.rb +37 -0
- data/lib/s3_relay/s3_relay.rb +4 -0
- data/lib/s3_relay/upload_presigner.rb +61 -0
- data/lib/s3_relay/version.rb +3 -0
- data/test/controllers/s3_relay/uploads_controller_test.rb +144 -0
- data/test/dummy/README.md +24 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/javascripts/cable.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/channels/application_cable/channel.rb +4 -0
- data/test/dummy/app/channels/application_cable/connection.rb +4 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/product.rb +6 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +9 -0
- data/test/dummy/bin/rake +9 -0
- data/test/dummy/bin/setup +38 -0
- data/test/dummy/bin/spring +17 -0
- data/test/dummy/bin/update +29 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +19 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/database.yml +22 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +54 -0
- data/test/dummy/config/environments/production.rb +91 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +56 -0
- data/test/dummy/config/routes.rb +7 -0
- data/test/dummy/config/secrets.yml +32 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/db/migrate/20141021002149_create_products.rb +9 -0
- data/test/dummy/db/schema.rb +41 -0
- data/test/dummy/db/seeds.rb +7 -0
- data/test/dummy/package.json +5 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/robots.txt +1 -0
- data/test/dummy/test/application_system_test_case.rb +5 -0
- data/test/dummy/test/test_helper.rb +9 -0
- data/test/factories/products.rb +5 -0
- data/test/factories/uploads.rb +23 -0
- data/test/helpers/s3_relay/uploads_helper_test.rb +29 -0
- data/test/lib/s3_relay/model_test.rb +66 -0
- data/test/lib/s3_relay/private_url_test.rb +28 -0
- data/test/lib/s3_relay/upload_presigner_test.rb +38 -0
- data/test/models/s3_relay/upload_test.rb +142 -0
- data/test/support/database_cleaner.rb +14 -0
- data/test/test_helper.rb +29 -0
- metadata +356 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
class S3Relay::UploadsController < ApplicationController
|
2
|
+
|
3
|
+
before_action :authenticate
|
4
|
+
skip_before_action :verify_authenticity_token
|
5
|
+
|
6
|
+
def new
|
7
|
+
render json: S3Relay::UploadPresigner.new.form_data
|
8
|
+
end
|
9
|
+
|
10
|
+
def create
|
11
|
+
@upload = S3Relay::Upload.new(upload_attrs)
|
12
|
+
|
13
|
+
if @upload.save
|
14
|
+
data = {
|
15
|
+
private_url: @upload.private_url,
|
16
|
+
parent_type: @upload.parent_type,
|
17
|
+
parent_id: @upload.parent_id,
|
18
|
+
user_id: user_attrs[:user_id]
|
19
|
+
}
|
20
|
+
render json: data, status: 201
|
21
|
+
else
|
22
|
+
render json: { errors: @upload.errors }, status: 422
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def authenticate
|
29
|
+
if respond_to?(:authenticate_for_s3_relay)
|
30
|
+
authenticate_for_s3_relay
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def parent_attrs
|
35
|
+
type = params[:parent_type].try(:classify)
|
36
|
+
id = params[:parent_id]
|
37
|
+
|
38
|
+
return {} unless type.present? && id.present? &&
|
39
|
+
parent = type.constantize.find(id)
|
40
|
+
|
41
|
+
begin
|
42
|
+
public_send(
|
43
|
+
"#{type.underscore.downcase}_#{params[:association]}_params",
|
44
|
+
parent
|
45
|
+
)
|
46
|
+
rescue NoMethodError
|
47
|
+
{ parent: parent }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def upload_attrs
|
52
|
+
attrs = {
|
53
|
+
upload_type: params[:association].try(:classify),
|
54
|
+
uuid: params[:uuid],
|
55
|
+
filename: params[:filename],
|
56
|
+
content_type: params[:content_type]
|
57
|
+
}
|
58
|
+
|
59
|
+
attrs.merge!(parent_attrs)
|
60
|
+
attrs.merge!(user_attrs)
|
61
|
+
end
|
62
|
+
|
63
|
+
def user_attrs
|
64
|
+
if respond_to?(:current_user) && (id = current_user&.id)
|
65
|
+
{ user_id: id }
|
66
|
+
else
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module S3Relay
|
2
|
+
module UploadsHelper
|
3
|
+
|
4
|
+
def s3_relay_field(parent, association, opts={})
|
5
|
+
file_field = file_field_tag(:file, opts.merge(class: "s3r-field"))
|
6
|
+
progress_table = content_tag(:table, "", class: "s3r-upload-list")
|
7
|
+
content = [file_field, progress_table].join
|
8
|
+
parent_type = parent.class.to_s.underscore.downcase
|
9
|
+
|
10
|
+
content_tag(:div, raw(content),
|
11
|
+
{
|
12
|
+
class: "s3r-container",
|
13
|
+
data: {
|
14
|
+
parent_type: parent_type,
|
15
|
+
parent_id: parent.id.to_s,
|
16
|
+
association: association.to_s,
|
17
|
+
disposition: opts.fetch(:disposition, "inline")
|
18
|
+
}
|
19
|
+
}
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class Upload
|
3
|
+
|
4
|
+
include Mongoid::Document
|
5
|
+
|
6
|
+
field :uuid
|
7
|
+
field :user_id, type: Integer
|
8
|
+
field :parent_type, type: String
|
9
|
+
field :parent_id, type: Integer
|
10
|
+
field :upload_type, type: String
|
11
|
+
field :filename, type: String
|
12
|
+
field :content_type, type: String
|
13
|
+
field :state, type: String
|
14
|
+
field :data, type: Hash, default: Hash.new
|
15
|
+
field :pending_at, type: DateTime
|
16
|
+
field :imported_at, type: DateTime
|
17
|
+
|
18
|
+
belongs_to :parent, polymorphic: true, optional: true
|
19
|
+
|
20
|
+
validates :uuid, presence: true, uniqueness: true
|
21
|
+
validates :upload_type, presence: true
|
22
|
+
validates :filename, presence: true
|
23
|
+
validates :content_type, presence: true
|
24
|
+
validates :pending_at, presence: true
|
25
|
+
|
26
|
+
after_initialize :finalize
|
27
|
+
after_create :notify_parent
|
28
|
+
|
29
|
+
def self.pending
|
30
|
+
where(state: "pending")
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.imported
|
34
|
+
where(state: "imported")
|
35
|
+
end
|
36
|
+
|
37
|
+
def pending?
|
38
|
+
state == "pending"
|
39
|
+
end
|
40
|
+
|
41
|
+
def imported?
|
42
|
+
state == "imported"
|
43
|
+
end
|
44
|
+
|
45
|
+
def mark_imported!
|
46
|
+
update_attributes(state: "imported", imported_at: Time.now)
|
47
|
+
end
|
48
|
+
|
49
|
+
def notify_parent
|
50
|
+
return unless parent.present?
|
51
|
+
|
52
|
+
if parent.respond_to?(:import_upload)
|
53
|
+
parent.import_upload(id)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def public_url
|
58
|
+
S3Relay::PrivateUrl.new(uuid, filename).public_url
|
59
|
+
end
|
60
|
+
|
61
|
+
def private_url
|
62
|
+
S3Relay::PrivateUrl.new(uuid, filename).generate
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def finalize
|
68
|
+
self.state ||= "pending"
|
69
|
+
self.pending_at ||= Time.now
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/config/routes.rb
ADDED
data/lib/s3_relay.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class Base
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def access_key_id
|
7
|
+
ENV["S3_RELAY_ACCESS_KEY_ID"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def secret_access_key
|
11
|
+
ENV["S3_RELAY_SECRET_ACCESS_KEY"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def region
|
15
|
+
ENV["S3_RELAY_REGION"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def bucket
|
19
|
+
ENV["S3_RELAY_BUCKET"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def acl
|
23
|
+
ENV["S3_RELAY_ACL"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def endpoint
|
27
|
+
"https://#{bucket}.s3-#{region}.amazonaws.com"
|
28
|
+
end
|
29
|
+
|
30
|
+
def digest
|
31
|
+
OpenSSL::Digest.new("sha1")
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace S3Relay
|
4
|
+
|
5
|
+
initializer "s3_relay.action_controller" do |app|
|
6
|
+
ActiveSupport.on_load :action_controller do
|
7
|
+
helper S3Relay::UploadsHelper
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require "s3_relay/base"
|
14
|
+
require "s3_relay/model"
|
15
|
+
require "s3_relay/private_url"
|
16
|
+
require "s3_relay/upload_presigner"
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module S3Relay
|
2
|
+
module Model
|
3
|
+
|
4
|
+
def s3_relay(attribute, has_many=false)
|
5
|
+
upload_type = attribute.to_s.classify
|
6
|
+
|
7
|
+
if has_many
|
8
|
+
has_many attribute, as: :parent, class_name: "S3Relay::Upload"
|
9
|
+
|
10
|
+
define_method attribute do
|
11
|
+
S3Relay::Upload
|
12
|
+
.where(
|
13
|
+
parent_type: self.class.to_s,
|
14
|
+
parent_id: self.id,
|
15
|
+
upload_type: upload_type
|
16
|
+
)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
has_one attribute, as: :parent, class_name: "S3Relay::Upload"
|
20
|
+
|
21
|
+
define_method attribute do
|
22
|
+
S3Relay::Upload
|
23
|
+
.where(
|
24
|
+
parent_type: self.class.to_s,
|
25
|
+
parent_id: self.id,
|
26
|
+
upload_type: upload_type
|
27
|
+
)
|
28
|
+
.order(:pending_at => 'desc').last
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
virtual_attribute = "new_#{attribute}_uuids"
|
33
|
+
attr_accessor virtual_attribute
|
34
|
+
|
35
|
+
association_method = "associate_#{attribute}"
|
36
|
+
|
37
|
+
after_save association_method.to_sym
|
38
|
+
|
39
|
+
define_method association_method do
|
40
|
+
new_uuids = send(virtual_attribute)
|
41
|
+
return if new_uuids.blank?
|
42
|
+
|
43
|
+
S3Relay::Upload.where(uuid: new_uuids, upload_type: upload_type)
|
44
|
+
.update_all(parent_type: self.class.to_s, parent_id: self.id)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class PrivateUrl < S3Relay::Base
|
3
|
+
|
4
|
+
attr_reader :expires, :path
|
5
|
+
|
6
|
+
def initialize(uuid, file, options={})
|
7
|
+
filename = Addressable::URI.escape(file).gsub("+", "%2B")
|
8
|
+
@path = [uuid, filename].join("/")
|
9
|
+
@expires = (options[:expires] || 10.minutes.from_now).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
"#{public_url}?#{params}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def public_url
|
17
|
+
"#{endpoint}/#{path}"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def params
|
23
|
+
[
|
24
|
+
"AWSAccessKeyId=#{access_key_id}",
|
25
|
+
"Expires=#{expires}",
|
26
|
+
"Signature=#{signature}"
|
27
|
+
].join("&")
|
28
|
+
end
|
29
|
+
|
30
|
+
def signature
|
31
|
+
string = "GET\n\n\n#{expires}\n/#{bucket}/#{path}"
|
32
|
+
hmac = OpenSSL::HMAC.digest(digest, secret_access_key, string)
|
33
|
+
CGI.escape(Base64.encode64(hmac).strip)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class UploadPresigner < S3Relay::Base
|
3
|
+
|
4
|
+
attr_reader :expires, :uuid
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
@expires = (options[:expires] || 1.minute.from_now).utc.xmlschema
|
8
|
+
@uuid = SecureRandom.uuid
|
9
|
+
end
|
10
|
+
|
11
|
+
def form_data
|
12
|
+
fields.keys.inject({}) { |h,k| h[k.downcase.underscore] = fields[k]; h }
|
13
|
+
.merge(
|
14
|
+
"endpoint" => endpoint,
|
15
|
+
"policy" => encoded_policy,
|
16
|
+
"signature" => signature,
|
17
|
+
"uuid" => uuid
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fields
|
24
|
+
{
|
25
|
+
"AWSAccessKeyID" => access_key_id,
|
26
|
+
"x-amz-server-side-encryption" => "AES256",
|
27
|
+
"key" => "#{uuid}/${filename}",
|
28
|
+
"success_action_status" => "201",
|
29
|
+
"acl" => acl
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def hmac
|
34
|
+
lambda { |data| OpenSSL::HMAC.digest(digest, secret_access_key, data) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def policy_document
|
38
|
+
{
|
39
|
+
"expiration" => expires,
|
40
|
+
"conditions" => [
|
41
|
+
{ "bucket" => bucket },
|
42
|
+
{ "acl" => acl },
|
43
|
+
{ "x-amz-server-side-encryption" => "AES256" },
|
44
|
+
{ "success_action_status" => "201" },
|
45
|
+
["starts-with", "$content-type", ""],
|
46
|
+
["starts-with", "$content-disposition", ""],
|
47
|
+
["starts-with", "$key", "#{uuid}/"]
|
48
|
+
]
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def encoded_policy
|
53
|
+
Base64.strict_encode64(policy_document.to_json)
|
54
|
+
end
|
55
|
+
|
56
|
+
def signature
|
57
|
+
Base64.strict_encode64(hmac[encoded_policy])
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module S3Relay
|
4
|
+
describe UploadsController do
|
5
|
+
before do
|
6
|
+
S3Relay::Base.any_instance.stubs(:access_key_id)
|
7
|
+
.returns("access-key-id")
|
8
|
+
S3Relay::Base.any_instance.stubs(:secret_access_key)
|
9
|
+
.returns("secret-access-key")
|
10
|
+
S3Relay::Base.any_instance.stubs(:region)
|
11
|
+
.returns("region")
|
12
|
+
S3Relay::Base.any_instance.stubs(:bucket)
|
13
|
+
.returns("bucket")
|
14
|
+
S3Relay::Base.any_instance.stubs(:acl)
|
15
|
+
.returns("acl")
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "GET new" do
|
19
|
+
it do
|
20
|
+
uuid = "123-456-789"
|
21
|
+
time = Time.parse("2014-12-01 12:00am")
|
22
|
+
S3Relay::UploadPresigner.any_instance.stubs(:uuid).returns(uuid)
|
23
|
+
S3Relay::UploadPresigner.any_instance.stubs(:expires).returns(time)
|
24
|
+
|
25
|
+
get new_s3_relay_upload_url
|
26
|
+
assert_response 200
|
27
|
+
|
28
|
+
data = JSON.parse(response.body)
|
29
|
+
|
30
|
+
data["awsaccesskeyid"].must_equal "access-key-id"
|
31
|
+
data["x_amz_server_side_encryption"].must_equal "AES256"
|
32
|
+
data["key"].must_equal "#{uuid}/${filename}"
|
33
|
+
data["success_action_status"].must_equal "201"
|
34
|
+
data["acl"].must_equal "acl"
|
35
|
+
data["endpoint"].must_equal "https://bucket.s3-region.amazonaws.com"
|
36
|
+
data["policy"].length.must_equal 380 # TODO: Improve this
|
37
|
+
data["signature"].length.must_equal 28 # TODO: Improve this
|
38
|
+
data["uuid"].must_equal uuid
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "POST create" do
|
43
|
+
|
44
|
+
describe "success" do
|
45
|
+
it do
|
46
|
+
assert_difference "S3Relay::Upload.count", 1 do
|
47
|
+
post s3_relay_uploads_url, params: {
|
48
|
+
association: "photo_uploads",
|
49
|
+
uuid: SecureRandom.uuid,
|
50
|
+
filename: "cat.png",
|
51
|
+
content_type: "image/png"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
assert_response 201
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "with parent attributes" do
|
59
|
+
describe "matching an object" do
|
60
|
+
before { @product = FactoryGirl.create(:product) }
|
61
|
+
|
62
|
+
it do
|
63
|
+
assert_difference "@product.photo_uploads.count", 1 do
|
64
|
+
post s3_relay_uploads_url, params: {
|
65
|
+
association: "photo_uploads",
|
66
|
+
uuid: SecureRandom.uuid,
|
67
|
+
filename: "cat.png",
|
68
|
+
content_type: "image/png",
|
69
|
+
parent_type: @product.class.to_s,
|
70
|
+
parent_id: @product.id.to_s
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
assert_response 201
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "not matching an object" do
|
79
|
+
it do
|
80
|
+
assert_difference "S3Relay::Upload.count" do
|
81
|
+
post s3_relay_uploads_url, params: {
|
82
|
+
association: "photo_uploads",
|
83
|
+
uuid: SecureRandom.uuid,
|
84
|
+
filename: "cat.png",
|
85
|
+
content_type: "image/png",
|
86
|
+
parent_type: "Product",
|
87
|
+
parent_id: "10000000"
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
assert_response 201
|
92
|
+
body = JSON.parse(response.body)
|
93
|
+
|
94
|
+
assert body["parent_type"] == nil
|
95
|
+
assert body["parent_id"] == nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "with a current_user" do
|
100
|
+
before do
|
101
|
+
@user = OpenStruct.new(id: 123)
|
102
|
+
UploadsController.any_instance.stubs(:current_user).returns(@user)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "associates the upload with the user" do
|
106
|
+
assert_difference "S3Relay::Upload.count", 1 do
|
107
|
+
post s3_relay_uploads_url, params: {
|
108
|
+
association: "photo_uploads",
|
109
|
+
uuid: SecureRandom.uuid,
|
110
|
+
filename: "cat.png",
|
111
|
+
content_type: "image/png"
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
assert_response 201
|
116
|
+
body = JSON.parse(response.body)
|
117
|
+
body["user_id"].must_equal @user.id
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "error" do
|
125
|
+
it do
|
126
|
+
assert_no_difference "S3Relay::Upload.count" do
|
127
|
+
post s3_relay_uploads_url, params: {
|
128
|
+
uuid: SecureRandom.uuid,
|
129
|
+
filename: "cat.png",
|
130
|
+
content_type: "image/png"
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
assert_response 422
|
135
|
+
|
136
|
+
JSON.parse(response.body)["errors"]["upload_type"]
|
137
|
+
.must_include "can't be blank"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|