Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve create picture validation #13697

Merged
merged 3 commits into from
Feb 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions app/controllers/api/pictures_controller.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
module Api
class PicturesController < BaseController
def create_resource(_type, _id, data)
data['content'] = Base64.decode64(data['content'])
picture = Picture.create(data)
raise BadRequestError,
"Failed to create Picture - #{picture.errors.full_messages.join(', ')}" unless picture.valid?
picture
Picture.create_from_base64(data)
rescue => err
raise BadRequestError, "Failed to create Picture - #{err}"
end
end
end
13 changes: 11 additions & 2 deletions app/models/picture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ class Picture < ApplicationRecord
has_one :binary_blob, :as => :resource, :dependent => :destroy, :autosave => true

validates :extension,
:inclusion => { :in => %w(png jpg svg), :message => 'must be a png, jpg, or svg' },
:if => :extension
:presence => true,
:inclusion => { :in => %w(png jpg svg), :message => 'must be a png, jpg, or svg' }
validates :content, :presence => true

virtual_has_one :image_href, :class_name => "String"

Expand Down Expand Up @@ -32,6 +33,14 @@ def self.url_path(filename, basepath = URL_ROOT)
end
end

def self.create_from_base64(attributes = {})
attributes = attributes.with_indifferent_access
new(attributes.except(:content)).tap do |picture|
picture.content = Base64.strict_decode64(attributes[:content].to_s)
picture.save!
end
end

def content
binary_blob.try(:binary)
end
Expand Down
4 changes: 4 additions & 0 deletions spec/factories/picture.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
FactoryGirl.define do
factory :picture do
extension 'png'
after(:build) do |x|
x.content = 'foo'
end
end
end
59 changes: 42 additions & 17 deletions spec/models/picture_spec.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
describe Picture do
subject { FactoryGirl.build :picture }
subject { FactoryGirl.build(:picture) }

it "auto-creates needed directory" do
expect(File.directory?(described_class.directory)).to be_truthy
end

it "#content" do
expect(subject.content).to be_nil
expected = "FOOBAR"
subject.content = expected.dup
expect(subject.content).to eq(expected)
context "#content" do
it 'returns expected content' do
expected = "FOOBAR"
subject.content = expected.dup
expect(subject.content).to eq(expected)
end

it 'requires content' do
subject.content = ''
expect(subject.valid?).to be_falsey
end
end

context "#extension" do
it 'is required' do
subject.extension = nil
expect(subject.valid?).to be_falsey
expect(subject.errors.messages).to eq(:extension => ["can't be blank", "must be a png, jpg, or svg"])
end

it "accepts only png, jpg, or svg" do
expect(subject.extension).to be_nil
subject.extension = "foo"

expect(subject.valid?).to be_falsey
Expand All @@ -31,7 +42,6 @@
end

it "on new record" do
expect(subject.extension).to be_nil
ext = "png"
subject.extension = ext.dup
expect(subject.extension).to eq(ext)
Expand All @@ -47,9 +57,8 @@

it "on existing record" do
subject.save

subject.reload
expect(subject.extension).to be_nil

ext = "jpg"
subject.extension = ext.dup
expect(subject.extension).to eq(ext)
Expand All @@ -65,9 +74,8 @@
end

it "#size" do
expect(subject.size).to eq(0)
expected = "FOOBAR"
subject.content = expected.dup
subject.content = expected.dup
expect(subject.size).to eq(expected.length)
end

Expand All @@ -77,11 +85,6 @@
end

context "works when record is saved" do
it "without extension" do
subject.save
expect(subject.basename).to eq("#{subject.compressed_id}.")
end

it "with extension" do
subject.extension = "png"
subject.save
Expand All @@ -90,6 +93,28 @@
end
end

context '.create_from_base64' do
let(:attributes) do
{
:extension => 'png',
:content => 'aW1hZ2U='
}
end

it 'creates a picture' do
expect do
Picture.create_from_base64(attributes)
end.to change(Picture, :count).by(1)
end

it 'requires valid base64' do
attributes[:content] = 'bogus'
expect do
Picture.create_from_base64(attributes)
end.to raise_error(StandardError, 'invalid base64')
end
end

it "#filename" do
basename = "foo.bar"
allow(subject).to receive(:basename).and_return(basename)
Expand Down
68 changes: 55 additions & 13 deletions spec/requests/api/picture_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,20 @@ def expect_result_to_include_picture_href(source_id)
end

describe 'POST /api/pictures' do
# one pixel png image encoded in Base64
# Valid base64 image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this looks like gibberish, I sort of liked this because it was a technically a valid image, giving anyone looking for documentation in these specs some idea of what to expect. The shorter, valid base 64 code above may suffice though. @abellotti any thoughts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@imtayadeway strict_decode64 had some issues with all of the newlines. Apparently it might not have been base64 😳

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense. So you'd need a really really really long line 😝

Or, you could use this pattern:

"foo"\
"bar"\
"baz" # => "foobarbaz"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, you could store the pixel as a fixture on the disk? I don't know.....could all be OTP

let(:content) do
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGP\n"\
"C/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3Cc\n"\
"ulE8AAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2Jl\n"\
"LnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIg\n"\
"eDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpy\n"\
"ZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1u\n"\
"cyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAg\n"\
"ICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYv\n"\
"MS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3Jp\n"\
"ZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpS\n"\
"REY+CjwveDp4bXBtZXRhPgpMwidZAAAADUlEQVQIHWNgYGCwBQAAQgA+3N0+\n"\
"xQAAAABJRU5ErkJggg==\n"
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGP"\
"C/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3Cc"\
"ulE8AAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2Jl"\
"LnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIg"\
"eDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpy"\
"ZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1u"\
"cyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAg"\
"ICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYv"\
"MS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3Jp"\
"ZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpS"\
"REY+CjwveDp4bXBtZXRhPgpMwidZAAAADUlEQVQIHWNgYGCwBQAAQgA+3N0+"\
"xQAAAABJRU5ErkJggg=="
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates @jntullo, question about above change that is driven by using strict_decode64. does this mean we just broke compatibility with v2.3.0 or the old usage with \n would never really occur ? How does the current service-ui generate the content in these payloads ? Thanks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abellotti the old usage with \n would never occur. When thinking about it, not really sure why I put that there in the first place, it's a mistake on my part. The current service-ui isn't uploading pictures via the pictures API - yet

end

it 'rejects create without an appropriate role' do
Expand Down Expand Up @@ -117,5 +117,47 @@ def expect_result_to_include_picture_href(source_id)
expect(response.parsed_body).to include(expected)
expect(response).to have_http_status(:ok)
end

it 'requires an extension' do
api_basic_authorize collection_action_identifier(:pictures, :create)

run_post pictures_url, :content => content

expected = {
'error' => a_hash_including(
'message' => a_string_including("Extension can't be blank")
)
}
expect(response).to have_http_status(:bad_request)
expect(response.parsed_body).to include(expected)
end

it 'requires content' do
api_basic_authorize collection_action_identifier(:pictures, :create)

run_post pictures_url, :extension => 'png'

expected = {
'error' => a_hash_including(
'message' => a_string_including("Content can't be blank")
)
}
expect(response).to have_http_status(:bad_request)
expect(response.parsed_body).to include(expected)
end

it 'requires content with valid base64' do
api_basic_authorize collection_action_identifier(:pictures, :create)

run_post pictures_url, :content => 'not base64', :extension => 'png'

expected = {
'error' => a_hash_including(
'message' => a_string_including('invalid base64')
)
}
expect(response).to have_http_status(:bad_request)
expect(response.parsed_body).to include(expected)
end
end
end