bulldog 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. data/.gitignore +2 -0
  2. data/DESCRIPTION.txt +3 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +64 -0
  6. data/VERSION +1 -0
  7. data/bulldog.gemspec +157 -0
  8. data/lib/bulldog.rb +95 -0
  9. data/lib/bulldog/attachment.rb +49 -0
  10. data/lib/bulldog/attachment/base.rb +167 -0
  11. data/lib/bulldog/attachment/has_dimensions.rb +94 -0
  12. data/lib/bulldog/attachment/image.rb +63 -0
  13. data/lib/bulldog/attachment/maybe.rb +229 -0
  14. data/lib/bulldog/attachment/none.rb +37 -0
  15. data/lib/bulldog/attachment/pdf.rb +63 -0
  16. data/lib/bulldog/attachment/unknown.rb +11 -0
  17. data/lib/bulldog/attachment/video.rb +143 -0
  18. data/lib/bulldog/error.rb +5 -0
  19. data/lib/bulldog/has_attachment.rb +214 -0
  20. data/lib/bulldog/interpolation.rb +73 -0
  21. data/lib/bulldog/missing_file.rb +12 -0
  22. data/lib/bulldog/processor.rb +5 -0
  23. data/lib/bulldog/processor/argument_tree.rb +116 -0
  24. data/lib/bulldog/processor/base.rb +124 -0
  25. data/lib/bulldog/processor/ffmpeg.rb +172 -0
  26. data/lib/bulldog/processor/image_magick.rb +134 -0
  27. data/lib/bulldog/processor/one_shot.rb +19 -0
  28. data/lib/bulldog/reflection.rb +234 -0
  29. data/lib/bulldog/saved_file.rb +19 -0
  30. data/lib/bulldog/stream.rb +186 -0
  31. data/lib/bulldog/style.rb +38 -0
  32. data/lib/bulldog/style_set.rb +101 -0
  33. data/lib/bulldog/tempfile.rb +28 -0
  34. data/lib/bulldog/util.rb +92 -0
  35. data/lib/bulldog/validations.rb +68 -0
  36. data/lib/bulldog/vector2.rb +18 -0
  37. data/rails/init.rb +9 -0
  38. data/script/console +8 -0
  39. data/spec/data/empty.txt +0 -0
  40. data/spec/data/test.jpg +0 -0
  41. data/spec/data/test.mov +0 -0
  42. data/spec/data/test.pdf +0 -0
  43. data/spec/data/test.png +0 -0
  44. data/spec/data/test2.jpg +0 -0
  45. data/spec/helpers/image_creation.rb +8 -0
  46. data/spec/helpers/temporary_directory.rb +25 -0
  47. data/spec/helpers/temporary_models.rb +76 -0
  48. data/spec/helpers/temporary_values.rb +102 -0
  49. data/spec/helpers/test_upload_files.rb +108 -0
  50. data/spec/helpers/time_travel.rb +20 -0
  51. data/spec/integration/data/test.jpg +0 -0
  52. data/spec/integration/lifecycle_hooks_spec.rb +213 -0
  53. data/spec/integration/processing_image_attachments.rb +72 -0
  54. data/spec/integration/processing_video_attachments_spec.rb +82 -0
  55. data/spec/integration/saving_an_attachment_spec.rb +31 -0
  56. data/spec/matchers/file_operations.rb +159 -0
  57. data/spec/spec_helper.rb +76 -0
  58. data/spec/unit/attachment/base_spec.rb +311 -0
  59. data/spec/unit/attachment/image_spec.rb +128 -0
  60. data/spec/unit/attachment/maybe_spec.rb +126 -0
  61. data/spec/unit/attachment/pdf_spec.rb +137 -0
  62. data/spec/unit/attachment/video_spec.rb +176 -0
  63. data/spec/unit/attachment_spec.rb +61 -0
  64. data/spec/unit/has_attachment_spec.rb +700 -0
  65. data/spec/unit/interpolation_spec.rb +108 -0
  66. data/spec/unit/processor/argument_tree_spec.rb +159 -0
  67. data/spec/unit/processor/ffmpeg_spec.rb +467 -0
  68. data/spec/unit/processor/image_magick_spec.rb +260 -0
  69. data/spec/unit/processor/one_shot_spec.rb +70 -0
  70. data/spec/unit/reflection_spec.rb +338 -0
  71. data/spec/unit/stream_spec.rb +234 -0
  72. data/spec/unit/style_set_spec.rb +44 -0
  73. data/spec/unit/style_spec.rb +51 -0
  74. data/spec/unit/validations_spec.rb +491 -0
  75. data/spec/unit/vector2_spec.rb +27 -0
  76. data/tasks/bulldog_tasks.rake +4 -0
  77. metadata +193 -0
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Processing video attachments" do
4
+ use_model_class(:Thing,
5
+ :video_file_name => :string,
6
+ :still_frame_file_name => :string)
7
+
8
+ before do
9
+ spec = self
10
+ Thing.class_eval do
11
+ has_attachment :video do
12
+ style :encoded, :format => 'ogg', :size => '640x360',
13
+ :video => 'libtheora 24fps', :audio => 'libvorbis 44100Hz 128kbps',
14
+ :pixel_format => 'yuv420p'
15
+ style :frame, :format => 'jpg'
16
+ path "#{spec.temporary_directory}/:id.:style.:extension"
17
+
18
+ # TODO: fix problem with :after => save running when the
19
+ # attachment wasn't changed, and process on after save.
20
+ process :on => :process, :styles => [:encoded]
21
+ process :on => :process, :styles => [:frame] do
22
+ record_frame(:assign_to => :still_frame)
23
+ end
24
+ end
25
+
26
+ has_attachment :still_frame do
27
+ style :thumbnail, :format => 'png'
28
+ path "#{spec.temporary_directory}/:id-still_frame.:style.:extension"
29
+ process :on => :process
30
+ end
31
+ end
32
+
33
+ @file = open("#{ROOT}/spec/data/test.mov")
34
+ @thing = Thing.new(:video => @file)
35
+ end
36
+
37
+ after do
38
+ @file.close
39
+ end
40
+
41
+ def original_video_path
42
+ "#{temporary_directory}/#{@thing.id}.original.mov"
43
+ end
44
+
45
+ def encoded_video_path
46
+ "#{temporary_directory}/#{@thing.id}.encoded.ogg"
47
+ end
48
+
49
+ def original_frame_path
50
+ "#{temporary_directory}/#{@thing.id}-still_frame.original.jpg"
51
+ end
52
+
53
+ def frame_thumbnail_path
54
+ "#{temporary_directory}/#{@thing.id}-still_frame.thumbnail.png"
55
+ end
56
+
57
+ it "should not yet have a still frame assigned" do
58
+ @thing.still_frame.should be_blank
59
+ end
60
+
61
+ describe "when the record is saved" do
62
+ before do
63
+ @thing.save
64
+ @thing.process_attachment(:video, :process)
65
+ @thing.process_attachment(:still_frame, :process)
66
+ end
67
+
68
+ it "should encode the video" do
69
+ File.should exist(original_video_path)
70
+ File.should exist(encoded_video_path)
71
+ end
72
+
73
+ it "should assign to the still frame" do
74
+ @thing.still_frame.should_not be_blank
75
+ end
76
+
77
+ it "should create the thumbnail" do
78
+ File.should exist(original_frame_path)
79
+ File.should exist(frame_thumbnail_path)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Saving an attachment" do
4
+ use_model_class(:Thing, :value => :integer)
5
+
6
+ #
7
+ # The list of files this process has open.
8
+ #
9
+ def open_files
10
+ `lsof -p #{Process.pid} -F n`.split(/\n/).sort
11
+ end
12
+
13
+ it "should not leave any file handles left open" do
14
+ tmp = temporary_directory
15
+ Thing.has_attachment :photo do
16
+ path "#{tmp}/:style.png"
17
+ style :small, :size => '10x10'
18
+ process :after => :save do
19
+ resize
20
+ end
21
+ end
22
+
23
+ path = create_image("#{temporary_directory}/tmp.jpg")
24
+ open(path) do |file|
25
+ @initial_open_files = open_files
26
+ thing = Thing.new(:photo => file)
27
+ thing.save.should be_true
28
+ open_files.should == @initial_open_files
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,159 @@
1
+ module Matchers
2
+ #
3
+ # Matches if the Proc object creates the file at the given path.
4
+ # The file also must not exist to begin with.
5
+ #
6
+ def create_file(path)
7
+ CreateFile.new(path)
8
+ end
9
+
10
+ #
11
+ # Matches if the Proc deletes the file at the given path. The file
12
+ # also must exist at the start.
13
+ #
14
+ def delete_file(path)
15
+ DeleteFile.new(path)
16
+ end
17
+
18
+ #
19
+ # Matches if the Proc modifies the file at the given path. Checks
20
+ # by examining the mtime of the file.
21
+ #
22
+ # In order to avoid false-positives, the mtime of the file is set to
23
+ # a temporary value during the block.
24
+ #
25
+ def modify_file(path)
26
+ ModifyFile.new(path)
27
+ end
28
+
29
+ class FileOperation
30
+ def initialize(path)
31
+ @path = path
32
+ end
33
+
34
+ attr_reader :path
35
+ end
36
+
37
+ class CreateFile < FileOperation
38
+ def matches?(proc)
39
+ @file_already_exists = File.exist?(path) and
40
+ return false
41
+ proc.call
42
+ File.exist?(path)
43
+ end
44
+
45
+ def does_not_match?(proc)
46
+ @file_already_exists = File.exist?(path) and
47
+ return false
48
+ proc.call
49
+ !File.exist?(path)
50
+ end
51
+
52
+ def failure_message_for_should
53
+ if @file_already_exists
54
+ "`#{path}' already exists"
55
+ else
56
+ "expected block to create `#{path}'"
57
+ end
58
+ end
59
+
60
+ def failure_message_for_should_not
61
+ if @file_already_exists
62
+ "`#{path}' already exists"
63
+ else
64
+ "expected block to not create `#{path}'"
65
+ end
66
+ end
67
+ end
68
+
69
+ class DeleteFile < FileOperation
70
+ def matches?(proc)
71
+ @file_did_not_exist = !File.exist?(path) and
72
+ return false
73
+ proc.call
74
+ !File.exist?(path)
75
+ end
76
+
77
+ def does_not_match?(proc)
78
+ @file_did_not_exist = !File.exist?(path) and
79
+ return false
80
+ proc.call
81
+ File.exist?(path)
82
+ end
83
+
84
+ def failure_message_for_should
85
+ if @file_did_not_exist
86
+ "`#{path}' does not exist"
87
+ else
88
+ "expected block to delete `#{path}'"
89
+ end
90
+ end
91
+
92
+ def failure_message_for_should_not
93
+ if @file_did_not_exist
94
+ "`#{path}' does not exist"
95
+ else
96
+ "expected block to not delete `#{path}'"
97
+ end
98
+ end
99
+ end
100
+
101
+ class ModifyFile < FileOperation
102
+ def matches?(proc)
103
+ @file_did_not_exist = !File.exist?(path) and
104
+ return false
105
+ modified?(proc)
106
+ end
107
+
108
+ def does_not_match?(proc)
109
+ @file_did_not_exist = !File.exist?(path) and
110
+ return false
111
+ !modified?(proc)
112
+ end
113
+
114
+ def failure_message_for_should
115
+ if @file_did_not_exist
116
+ "`#{path}' does not exist"
117
+ else
118
+ "expected block to modify `#{path}'"
119
+ end
120
+ end
121
+
122
+ def failure_message_for_should_not
123
+ if @file_did_not_exist
124
+ "`#{path}' does not exist"
125
+ else
126
+ "expected block to not modify `#{path}'"
127
+ end
128
+ end
129
+
130
+ private # -------------------------------------------------------
131
+
132
+ def modified?(proc)
133
+ temporarily_setting_mtime_to(1.minute.ago) do
134
+ start_mtime = mtime
135
+ proc.call
136
+ end_mtime = mtime
137
+ start_mtime.to_i != end_mtime.to_i
138
+ end
139
+ end
140
+
141
+ def temporarily_setting_mtime_to(time)
142
+ original_time = mtime
143
+ set_mtime_to(time)
144
+ yield
145
+ ensure
146
+ set_mtime_to(original_time)
147
+ end
148
+
149
+ def mtime
150
+ File.mtime(path)
151
+ end
152
+
153
+ def set_mtime_to(time)
154
+ atime = File.atime(path)
155
+ File.utime(atime, time, path)
156
+ time
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,76 @@
1
+ require 'fileutils'
2
+ require 'ostruct'
3
+
4
+ require 'spec'
5
+ require 'rspec_outlines'
6
+ require 'mocha'
7
+ require 'tempfile'
8
+ require 'active_record'
9
+ require 'action_controller'
10
+
11
+ require 'bulldog'
12
+ include Bulldog
13
+
14
+ ROOT = File.dirname( File.dirname(__FILE__) )
15
+
16
+ require 'helpers/time_travel'
17
+ require 'helpers/temporary_models'
18
+ require 'helpers/temporary_values'
19
+ require 'helpers/temporary_directory'
20
+ require 'helpers/test_upload_files'
21
+ require 'helpers/image_creation'
22
+ require 'matchers/file_operations'
23
+
24
+ class Time
25
+ #
26
+ # Return a new Time object with subsecond components dropped.
27
+ #
28
+ # This is useful for testing Time values that have been roundtripped
29
+ # through the database, as not all databases store subsecond
30
+ # precision.
31
+ #
32
+ def drop_subseconds
33
+ self.class.mktime(year, month, day, hour, min, sec)
34
+ end
35
+ end
36
+
37
+ module SpecHelper
38
+ def self.included(mod)
39
+ mod.use_temporary_attribute_value Bulldog, :default_url_template do
40
+ ":class/:id.:style"
41
+ end
42
+ mod.use_temporary_attribute_value Bulldog, :default_path_template do
43
+ "#{temporary_directory}/attachments/:class/:id.:style"
44
+ end
45
+
46
+ mod.use_temporary_attribute_value Bulldog, :logger do
47
+ buffer = StringIO.new
48
+ logger = Logger.new(buffer)
49
+ (class << logger; self; end).send(:define_method, :content) do
50
+ buffer.string
51
+ end
52
+ logger
53
+ end
54
+ end
55
+
56
+ #
57
+ # Stub out all system calls. Pretend they were successful.
58
+ #
59
+ def stub_system_calls
60
+ Kernel.stubs(:system).returns(true)
61
+ end
62
+ end
63
+
64
+ Spec::Runner.configure do |config|
65
+ config.mock_with :mocha
66
+ config.include TimeTravel
67
+ config.include TemporaryModels
68
+ config.include TemporaryValues
69
+ config.include TemporaryDirectory
70
+ config.include TestUploadFiles
71
+ config.include ImageCreation
72
+ config.include Matchers
73
+ config.include SpecHelper
74
+ end
75
+
76
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
@@ -0,0 +1,311 @@
1
+ require 'spec_helper'
2
+
3
+ describe Attachment::Base do
4
+ def self.configure_attachment(&block)
5
+ before do
6
+ spec = self
7
+ Thing.has_attachment :photo do
8
+ instance_exec(spec, &block)
9
+ end
10
+ @thing = Thing.new
11
+ end
12
+ end
13
+
14
+ describe "#path" do
15
+ use_model_class(:Thing, :photo_file_name => :string)
16
+
17
+ configure_attachment do |spec|
18
+ path "#{spec.temporary_directory}/:style.jpg"
19
+ style :small, {}
20
+ style :png, :format => :png
21
+ end
22
+
23
+ def original_path
24
+ "#{temporary_directory}/original.jpg"
25
+ end
26
+
27
+ def small_path
28
+ "#{temporary_directory}/small.jpg"
29
+ end
30
+
31
+ def png_path
32
+ "#{temporary_directory}/png.png"
33
+ end
34
+
35
+ it "should return the path of the given style, interpolated from the path template" do
36
+ @thing.photo = test_image_file
37
+ @thing.stubs(:id).returns(5)
38
+ @thing.photo.path(:original).should == original_path
39
+ @thing.photo.path(:small).should == small_path
40
+ end
41
+
42
+ describe "when the :extension interpolation key is used" do
43
+ before do
44
+ spec = self
45
+ Thing.attachment_reflections[:photo].configure do
46
+ path "#{spec.temporary_directory}/:style.:extension"
47
+ end
48
+ @thing.photo = test_image_file
49
+ end
50
+
51
+ it "should use the extension of the original file for the original style" do
52
+ @thing.photo.path(:original).should == "#{temporary_directory}/original.jpg"
53
+ end
54
+ it "should use the format of the output file for other styles" do
55
+ @thing.photo.path(:png).should == "#{temporary_directory}/png.png"
56
+ end
57
+ end
58
+
59
+ describe "when the :extension interpolation key is not used" do
60
+ before do
61
+ spec = self
62
+ Thing.attachment_reflections[:photo].configure do
63
+ path "#{spec.temporary_directory}/:style.xyz"
64
+ end
65
+ @thing.photo = test_image_file
66
+ end
67
+
68
+ it "should use the extension of the path template for the original style" do
69
+ @thing.photo.path(:original).should == "#{temporary_directory}/original.xyz"
70
+ end
71
+
72
+ it "should use the extension of the path template for other styles" do
73
+ @thing.photo.path(:png).should == "#{temporary_directory}/png.xyz"
74
+ end
75
+ end
76
+
77
+ describe "when no style is given" do
78
+ configure_attachment do
79
+ path "/tmp/:style.jpg"
80
+ style :small, {}
81
+ default_style :small
82
+ end
83
+
84
+ it "should default to the attachment's default style" do
85
+ @thing.photo = test_image_file
86
+ @thing.photo.path.should == "/tmp/small.jpg"
87
+ end
88
+ end
89
+ end
90
+
91
+ describe "#url" do
92
+ use_model_class(:Thing, :photo_file_name => :string)
93
+
94
+ configure_attachment do
95
+ path "/tmp/:style.jpg"
96
+ url "/assets/:style.jpg"
97
+ style :small
98
+ style :png, :format => :png
99
+ end
100
+
101
+ it "should return the url of the given style, interpolated from the url template" do
102
+ @thing.photo = test_image_file
103
+ @thing.photo.url(:original).should == "/assets/original.jpg"
104
+ @thing.photo.url(:small).should == "/assets/small.jpg"
105
+ end
106
+
107
+ describe "when the :extension interpolation key is used" do
108
+ before do
109
+ spec = self
110
+ Thing.attachment_reflections[:photo].configure do
111
+ path "/tmp/:style.:extension"
112
+ url "/assets/:style.:extension"
113
+ end
114
+ @thing.photo = test_image_file
115
+ end
116
+
117
+ it "should use the extension of the original file for the original style" do
118
+ @thing.photo.url(:original).should == "/assets/original.jpg"
119
+ end
120
+
121
+ it "should use the format of the output file for the other styles" do
122
+ @thing.photo.url(:png).should == "/assets/png.png"
123
+ end
124
+ end
125
+
126
+ describe "when the :extension interpolation key is not used" do
127
+ before do
128
+ spec = self
129
+ Thing.attachment_reflections[:photo].configure do
130
+ path "/tmp/:style.xyz"
131
+ url "/assets/:style.xyz"
132
+ end
133
+ @thing.photo = test_image_file
134
+ end
135
+
136
+ it "should use the extension of the url template for the original style" do
137
+ @thing.photo.url(:original).should == "/assets/original.xyz"
138
+ end
139
+
140
+ it "should use the extension of the url template for the other styles" do
141
+ @thing.photo.url(:png).should == "/assets/png.xyz"
142
+ end
143
+ end
144
+
145
+ describe "when no style is given" do
146
+ configure_attachment do
147
+ url "/assets/:style.jpg"
148
+ style :small, {}
149
+ default_style :small
150
+ end
151
+
152
+ it "should default to the attachment's default style" do
153
+ @thing.photo = test_image_file
154
+ @thing.photo.url.should == "/assets/small.jpg"
155
+ end
156
+ end
157
+ end
158
+
159
+ describe "#file_size" do
160
+ use_model_class(:Thing)
161
+
162
+ configure_attachment do |spec|
163
+ path "#{spec.temporary_directory}/:id.:style.jpg"
164
+ style :small, {}
165
+ end
166
+
167
+ def original_path
168
+ "#{temporary_directory}/#{@thing.id}.original.jpg"
169
+ end
170
+
171
+ def with_temporary_file(path, content)
172
+ FileUtils.mkdir_p File.dirname(path)
173
+ open(path, 'w'){|f| f.print '...'}
174
+ begin
175
+ yield path
176
+ ensure
177
+ File.delete(path)
178
+ end
179
+ end
180
+
181
+ before do
182
+ @thing = Thing.new(:photo => test_image_file)
183
+ end
184
+
185
+ it "should return the size of the file" do
186
+ @thing.photo.file_size.should == File.size(test_image_path)
187
+ end
188
+ end
189
+
190
+ describe "#file_name" do
191
+ use_model_class(:Thing, :photo_file_name => :string)
192
+
193
+ configure_attachment do |spec|
194
+ path "#{spec.temporary_directory}/:id.:style.jpg"
195
+ style :small, {}
196
+ store_attributes :file_name
197
+ end
198
+
199
+ def original_path
200
+ "#{temporary_directory}/#{@thing.id}.original.jpg"
201
+ end
202
+
203
+ def with_temporary_file(path, content)
204
+ FileUtils.mkdir_p File.dirname(path)
205
+ open(path, 'w'){|f| f.print '...'}
206
+ begin
207
+ yield path
208
+ ensure
209
+ File.delete(path)
210
+ end
211
+ end
212
+
213
+ before do
214
+ @thing = Thing.new(:photo => test_image_file)
215
+ end
216
+
217
+ it "should return the original base name of the file" do
218
+ @thing.photo.file_name.should == File.basename(test_image_path)
219
+ end
220
+ end
221
+
222
+ describe "#process" do
223
+ use_model_class(:Thing)
224
+
225
+ use_temporary_constant_value Processor, :Test do
226
+ Class.new(Processor::Base)
227
+ end
228
+
229
+ it "should use the default processor if no processor was specified" do
230
+ context = nil
231
+ Thing.has_attachment :photo do
232
+ style :normal
233
+ process :on => :test_event do
234
+ context = self
235
+ end
236
+ end
237
+ thing = Thing.new(:photo => test_image_file)
238
+ thing.photo.stubs(:default_processor_type).returns(:test)
239
+ thing.photo.process(:test_event)
240
+ context.should be_a(Processor::Test)
241
+ end
242
+
243
+ it "should use the configured processor if one was specified" do
244
+ context = nil
245
+ Thing.has_attachment :photo do
246
+ style :normal
247
+ process :on => :test_event, :with => :test do
248
+ context = self
249
+ end
250
+ end
251
+ thing = Thing.new(:photo => test_image_file)
252
+ thing.photo.process(:test_event)
253
+ context.should be_a(Processor::Test)
254
+ end
255
+
256
+ it "should not run any processors if no attachment is set" do
257
+ run = false
258
+ Thing.has_attachment :photo do
259
+ style :normal
260
+ process :on => :test_event, :with => :test do
261
+ run = true
262
+ end
263
+ end
264
+ thing = Thing.new(:photo => nil)
265
+ thing.photo.process(:test_event)
266
+ run.should be_false
267
+ end
268
+
269
+ it "should run the processors only for the specified styles" do
270
+ styles = nil
271
+ Thing.has_attachment :photo do
272
+ style :small, :size => '10x10'
273
+ style :large, :size => '1000x1000'
274
+ process :on => :test_event, :styles => [:small], :with => :test do
275
+ styles = self.styles
276
+ end
277
+ end
278
+ thing = Thing.new(:photo => test_image_file)
279
+ thing.photo.process(:test_event)
280
+ styles.should be_a(StyleSet)
281
+ styles.map(&:name).should == [:small]
282
+ end
283
+ end
284
+
285
+ describe "storable attributes" do
286
+ use_model_class(:Thing,
287
+ :photo_file_name => :string,
288
+ :photo_file_size => :integer,
289
+ :photo_content_type => :string)
290
+
291
+ before do
292
+ Thing.has_attachment :photo
293
+ @thing = Thing.new(:photo => uploaded_file_with_content('test.jpg', "\xff\xd8"))
294
+ end
295
+
296
+ it "should set the stored attributes on assignment" do
297
+ @thing.photo_file_name.should == 'test.jpg'
298
+ @thing.photo_file_size.should == 2
299
+ @thing.photo_content_type.should =~ /image\/jpeg/
300
+ end
301
+
302
+ it "should successfully roundtrip the stored attributes" do
303
+ warp_ahead 1.minute
304
+ @thing.save
305
+ @thing = Thing.find(@thing.id)
306
+ @thing.photo_file_name.should == 'test.jpg'
307
+ @thing.photo_file_size.should == 2
308
+ @thing.photo_content_type.should =~ /image\/jpeg/
309
+ end
310
+ end
311
+ end