bulldog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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