test: cover MultipartTest, build_body and OrmAttributesTest
This commit is contained in:
1 parent
3fe62a3f2f
commit
845f341fe2
2 files changed
+205
No files matched your search
@@ -0,0 +1,82 @@
|
|||||||
|
require 'minitest/autorun'
|
||||||
|
require 'prism'
|
||||||
|
|
||||||
|
class MultipartTest < Minitest::Test
|
||||||
|
BOUNDARY = '----PrismTestBoundary7331'
|
||||||
|
|
||||||
|
def build_body(fields: {}, files: {})
|
||||||
|
parts = fields.map do |name, value|
|
||||||
|
"--#{BOUNDARY}\r\nContent-Disposition: form-data; name=\"#{name}\"\r\n\r\n#{value}\r\n"
|
||||||
|
end
|
||||||
|
parts += files.map do |name, spec|
|
||||||
|
filename, content_type, content = spec
|
||||||
|
"--#{BOUNDARY}\r\nContent-Disposition: form-data; name=\"#{name}\"; filename=\"#{filename}\"\r\n" \
|
||||||
|
"Content-Type: #{content_type}\r\n\r\n#{content}\r\n"
|
||||||
|
end
|
||||||
|
parts.join + "--#{BOUNDARY}--\r\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_multipart_detects_content_type
|
||||||
|
assert Prism::Multipart.multipart?('multipart/form-data; boundary=abc')
|
||||||
|
refute Prism::Multipart.multipart?('application/json')
|
||||||
|
refute Prism::Multipart.multipart?(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_boundary_from_quoted_and_bare
|
||||||
|
assert_equal 'abc123', Prism::Multipart.boundary_from('multipart/form-data; boundary="abc123"')
|
||||||
|
assert_equal 'abc123', Prism::Multipart.boundary_from('multipart/form-data; boundary=abc123')
|
||||||
|
assert_nil Prism::Multipart.boundary_from('multipart/form-data')
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parses_plain_fields
|
||||||
|
body = build_body(fields: { 'title' => 'hello world', 'count' => '3' })
|
||||||
|
result = Prism::Multipart.parse(body, BOUNDARY)
|
||||||
|
assert_equal 'hello world', result['title']
|
||||||
|
assert_equal '3', result['count']
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parses_uploaded_file
|
||||||
|
body = build_body(
|
||||||
|
fields: { 'title' => 'my post' },
|
||||||
|
files: { 'avatar' => ['avatar.png', 'image/png', "\x89PNG\r\ndata\x00"] }
|
||||||
|
)
|
||||||
|
result = Prism::Multipart.parse(body, BOUNDARY)
|
||||||
|
assert_equal 'my post', result['title']
|
||||||
|
|
||||||
|
file = result['avatar']
|
||||||
|
assert_instance_of Prism::UploadedFile, file
|
||||||
|
assert_equal 'avatar.png', file.original_filename
|
||||||
|
assert_equal 'image/png', file.content_type
|
||||||
|
assert_equal "\x89PNG\r\ndata\x00".b, file.read
|
||||||
|
assert_equal file.size, File.size(file.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_binary_content_is_preserved_byte_for_byte
|
||||||
|
binary = (0..255).map(&:chr).join.b
|
||||||
|
body = build_body(files: { 'blob' => ['blob.bin', 'application/octet-stream', binary] })
|
||||||
|
result = Prism::Multipart.parse(body, BOUNDARY)
|
||||||
|
assert_equal binary, result['blob'].read
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_array_field_names_collect_into_arrays
|
||||||
|
parts = [
|
||||||
|
"--#{BOUNDARY}\r\nContent-Disposition: form-data; name=\"tags[]\"\r\n\r\nruby\r\n",
|
||||||
|
"--#{BOUNDARY}\r\nContent-Disposition: form-data; name=\"tags[]\"\r\n\r\nweb\r\n",
|
||||||
|
"--#{BOUNDARY}--\r\n"
|
||||||
|
]
|
||||||
|
result = Prism::Multipart.parse(parts.join, BOUNDARY)
|
||||||
|
assert_equal %w[ruby web], result['tags']
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_empty_boundary_returns_empty_hash
|
||||||
|
assert_equal({}, Prism::Multipart.parse('anything', nil))
|
||||||
|
assert_equal({}, Prism::Multipart.parse('anything', ''))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ignores_preamble_and_epilogue
|
||||||
|
body = "this is a preamble that must be ignored\r\n" + build_body(fields: { 'a' => '1' }) + "trailing epilogue"
|
||||||
|
result = Prism::Multipart.parse(body, BOUNDARY)
|
||||||
|
assert_equal '1', result['a']
|
||||||
|
assert_equal 1, result.size
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
require 'minitest/autorun'
|
||||||
|
require 'prism'
|
||||||
|
require 'tmpdir'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
class OrmAttributesTest < Minitest::Test
|
||||||
|
def setup
|
||||||
|
@dir = Dir.mktmpdir('prism-attributes-store')
|
||||||
|
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
|
||||||
|
@store.create_table(:widgets, %w[id title featured views price published_at])
|
||||||
|
widget_class = Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :widgets
|
||||||
|
attribute :featured, :boolean, default: false
|
||||||
|
attribute :views, :integer
|
||||||
|
attribute :price, :float
|
||||||
|
attribute :published_at, :datetime
|
||||||
|
end
|
||||||
|
widget_class.store = @store
|
||||||
|
Object.const_set(:Widget, widget_class)
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
FileUtils.remove_entry(@dir)
|
||||||
|
Object.send(:remove_const, :Widget) if Object.const_defined?(:Widget)
|
||||||
|
Object.send(:remove_const, :Gadget) if Object.const_defined?(:Gadget)
|
||||||
|
Object.send(:remove_const, :SpecialGadget) if Object.const_defined?(:SpecialGadget)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_string_form_input_casts_to_the_declared_type_on_write
|
||||||
|
widget = Widget.new(featured: '1', views: '42', price: '9.5')
|
||||||
|
assert_equal true, widget.featured
|
||||||
|
assert_equal 42, widget.views
|
||||||
|
assert_in_delta 9.5, widget.price
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cast_value_is_what_actually_gets_stored_not_the_raw_string
|
||||||
|
widget = Widget.create!(title: 'a', featured: '1', views: '42')
|
||||||
|
assert_equal true, widget.to_h['featured']
|
||||||
|
assert_equal 42, widget.to_h['views']
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_default_is_applied_when_the_attribute_is_omitted
|
||||||
|
assert_equal false, Widget.new(title: 'a').featured
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_an_explicit_value_wins_over_the_default
|
||||||
|
assert_equal true, Widget.new(title: 'a', featured: 'yes').featured
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_default_survives_a_real_save_and_reload
|
||||||
|
widget = Widget.create!(title: 'a')
|
||||||
|
reloaded = Widget.find(widget.id)
|
||||||
|
assert_equal false, reloaded.featured
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_proc_default_is_evaluated_per_instance
|
||||||
|
Object.const_set(:Gadget, Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :widgets
|
||||||
|
attribute :views, :integer, default: -> { 7 }
|
||||||
|
end)
|
||||||
|
Gadget.store = @store
|
||||||
|
assert_equal 7, Gadget.new(title: 'a').views
|
||||||
|
assert_equal 7, Gadget.new(title: 'b').views
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reloading_a_saved_record_returns_real_typed_values_not_strings
|
||||||
|
widget = Widget.create!(title: 'a', featured: '1', views: '42', price: '9.5')
|
||||||
|
reloaded = Widget.find(widget.id)
|
||||||
|
assert_equal true, reloaded.featured
|
||||||
|
assert_kind_of Integer, reloaded.views
|
||||||
|
assert_equal 42, reloaded.views
|
||||||
|
assert_kind_of Float, reloaded.price
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_getter_defensively_casts_a_legacy_raw_value_never_written_through_the_setter
|
||||||
|
id = @store.insert(:widgets, 'title' => 'legacy', 'featured' => '1', 'views' => '3')
|
||||||
|
legacy = Widget.find(id)
|
||||||
|
assert_equal true, legacy.featured
|
||||||
|
assert_equal 3, legacy.views
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_datetime_attribute_parses_an_iso8601_string
|
||||||
|
widget = Widget.new(title: 'a', published_at: '2026-02-01T12:00:00Z')
|
||||||
|
assert_kind_of Time, widget.published_at
|
||||||
|
assert_equal 2026, widget.published_at.year
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_invalid_input_raises_invalid_cast_instead_of_saving_garbage
|
||||||
|
assert_raises(Prism::Orm::TypeCasting::InvalidCast) { Widget.new(title: 'a', views: 'not-a-number') }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_serializable_hash_reflects_the_cast_type_not_the_raw_string
|
||||||
|
widget = Widget.create!(title: 'a', featured: '1', views: '42')
|
||||||
|
payload = JSON.parse(widget.to_json)
|
||||||
|
assert_equal true, payload['featured']
|
||||||
|
assert_equal 42, payload['views']
|
||||||
|
end
|
||||||
|
|
||||||
|
# Same regression shape as Callbacks#callbacks_for / Controller#before_actions
|
||||||
|
# (documented in this project's history as a bug found three separate
|
||||||
|
# times: a class-level registry stored as a plain `@ivar ||= {}` per
|
||||||
|
# class, with no superclass fallback, silently drops whatever a base
|
||||||
|
# class declared as soon as a subclass declares its own). attribute_types/
|
||||||
|
# attribute_defaults use the same superclass-plus-own merge, so this
|
||||||
|
# proves a subclass's own `attribute` call does not erase its parent's.
|
||||||
|
def test_subclass_inherits_the_parent_classes_declared_attributes
|
||||||
|
Object.const_set(:Gadget, Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :widgets
|
||||||
|
attribute :featured, :boolean, default: false
|
||||||
|
end)
|
||||||
|
Object.const_set(:SpecialGadget, Class.new(Gadget) do
|
||||||
|
table_name :widgets
|
||||||
|
attribute :views, :integer, default: 9
|
||||||
|
end)
|
||||||
|
SpecialGadget.store = @store
|
||||||
|
|
||||||
|
assert_equal %i[boolean integer].sort_by(&:to_s), [SpecialGadget.attribute_types['featured'],
|
||||||
|
SpecialGadget.attribute_types['views']].sort_by(&:to_s)
|
||||||
|
special = SpecialGadget.new(title: 'a')
|
||||||
|
assert_equal false, special.featured, 'inherited attribute default was dropped by the subclass'
|
||||||
|
assert_equal 9, special.views
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user