seed_dump 3.3.1 → 3.4.0

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.
@@ -2,175 +2,1113 @@ require 'spec_helper'
2
2
 
3
3
  describe SeedDump do
4
4
 
5
- def expected_output(include_id = false, id_offset = 0)
5
+ # Helper for expected output based on default factory values (integer: 42)
6
+ # Uses ISO 8601 format with timezone suffix (issue #111)
7
+ def expected_output(include_id = false, id_offset = 0, count = 3)
6
8
  output = "Sample.create!([\n "
7
-
8
9
  data = []
9
- ((1 + id_offset)..(3 + id_offset)).each do |i|
10
- data << "{#{include_id ? "id: #{i}, " : ''}string: \"string\", text: \"text\", integer: 42, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04 19:14:00\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false}"
10
+ start_id = 1 + id_offset
11
+ end_id = count + id_offset # Adjust end based on count
12
+ (start_id..end_id).each do |i|
13
+ # Expect integer: 42, ISO 8601 format with timezone
14
+ data << "{#{include_id ? "id: #{i}, " : ''}string: \"string\", text: \"text\", integer: 42, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04T19:14:00Z\", time: \"2000-01-01T03:15:00Z\", date: \"1863-11-19\", binary: \"binary\", boolean: false}"
11
15
  end
12
-
13
16
  output + data.join(",\n ") + "\n])\n"
14
17
  end
15
18
 
16
- describe '.dump' do
17
- before do
18
- Rails.application.eager_load!
19
+ # Helper for activerecord-import output based on default factory values
20
+ # Uses ISO 8601 format with timezone suffix (issue #111)
21
+ def expected_import_output(exclude_id_timestamps = true)
22
+ columns = if exclude_id_timestamps
23
+ [:string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean]
24
+ else
25
+ [:id, :string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean, :created_at, :updated_at]
26
+ end
27
+ output = "Sample.import([#{columns.map(&:inspect).join(', ')}], [\n "
28
+ data = []
29
+ (1..3).each do |i|
30
+ row = if exclude_id_timestamps
31
+ # Expect integer: 42, ISO 8601 format with timezone
32
+ ["string", "text", 42, 3.14, "2.72", "1776-07-04T19:14:00Z", "2000-01-01T03:15:00Z", "1863-11-19", "binary", false]
33
+ else
34
+ # Expect integer: 42, ISO 8601 format with timezone
35
+ [i, "string", "text", 42, 3.14, "2.72", "1776-07-04T19:14:00Z", "2000-01-01T03:15:00Z", "1863-11-19", "binary", false, "1969-07-20T20:18:00Z", "1989-11-10T04:20:00Z"]
36
+ end
37
+ data << "[#{row.map(&:inspect).join(', ')}]"
38
+ end
39
+ output + data.join(",\n ") + "\n])\n"
40
+ end
19
41
 
20
- create_db
42
+ # Helper for activerecord-import output with options
43
+ # Uses ISO 8601 format with timezone suffix (issue #111)
44
+ def expected_import_output_with_options
45
+ columns = [:id, :string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean, :created_at, :updated_at]
46
+ output = "Sample.import([#{columns.map(&:inspect).join(', ')}], [\n "
47
+ data = []
48
+ (1..3).each do |i|
49
+ # Expect integer: 42, ISO 8601 format with timezone
50
+ row = [i, "string", "text", 42, 3.14, "2.72", "1776-07-04T19:14:00Z", "2000-01-01T03:15:00Z", "1863-11-19", "binary", false, "1969-07-20T20:18:00Z", "1989-11-10T04:20:00Z"]
51
+ data << "[#{row.map(&:inspect).join(', ')}]"
52
+ end
53
+ output + data.join(",\n ") + "\n], validate: false)\n"
54
+ end
21
55
 
22
- FactoryBot.create_list(:sample, 3)
56
+ # Helper for insert_all output based on default factory values (issue #153)
57
+ # Uses ISO 8601 format with timezone suffix (issue #111)
58
+ def expected_insert_all_output(exclude_id_timestamps = true)
59
+ output = "Sample.insert_all([\n "
60
+ data = []
61
+ (1..3).each do |i|
62
+ row = if exclude_id_timestamps
63
+ # Expect integer: 42, ISO 8601 format with timezone
64
+ { string: "string", text: "text", integer: 42, float: 3.14, decimal: "2.72", datetime: "1776-07-04T19:14:00Z", time: "2000-01-01T03:15:00Z", date: "1863-11-19", binary: "binary", boolean: false }
65
+ else
66
+ { id: i, string: "string", text: "text", integer: 42, float: 3.14, decimal: "2.72", datetime: "1776-07-04T19:14:00Z", time: "2000-01-01T03:15:00Z", date: "1863-11-19", binary: "binary", boolean: false, created_at: "1969-07-20T20:18:00Z", updated_at: "1989-11-10T04:20:00Z" }
67
+ end
68
+ data << "{#{row.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}}"
23
69
  end
70
+ output + data.join(",\n ") + "\n])\n"
71
+ end
72
+
73
+
74
+ describe '.dump' do
24
75
 
25
76
  context 'without file option' do
77
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
26
78
  it 'should return the dump of the models passed in' do
27
- SeedDump.dump(Sample).should eq(expected_output)
79
+ expect(SeedDump.dump(Sample)).to eq(expected_output) # Expects 3 standard samples
28
80
  end
29
81
  end
30
82
 
31
83
  context 'with file option' do
32
- before do
33
- @filename = Tempfile.new(File.join(Dir.tmpdir, 'foo'), nil)
34
- end
84
+ let(:tempfile) { Tempfile.new(['seed_dump_test', '.rb']) }
85
+ let(:filename) { tempfile.path }
86
+
87
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
35
88
 
36
89
  after do
37
- File.unlink(@filename)
90
+ tempfile.close
91
+ tempfile.unlink
38
92
  end
39
93
 
40
94
  it 'should dump the models to the specified file' do
41
- SeedDump.dump(Sample, file: @filename)
42
-
43
- File.open(@filename) { |file| file.read.should eq(expected_output) }
95
+ SeedDump.dump(Sample, file: filename)
96
+ expect(File.read(filename)).to eq(expected_output) # Expects 3 standard samples
44
97
  end
45
98
 
46
99
  context 'with append option' do
47
100
  it 'should append to the file rather than overwriting it' do
48
- SeedDump.dump(Sample, file: @filename)
49
- SeedDump.dump(Sample, file: @filename, append: true)
101
+ # before(:each) creates 3 records
102
+ SeedDump.dump(Sample, file: filename) # Dumps the 3 records
103
+ # Second dump should dump the same 3 records again
104
+ SeedDump.dump(Sample, file: filename, append: true)
105
+ expect(File.read(filename)).to eq(expected_output + expected_output) # Expects 2 sets of 3 standard samples
106
+ end
107
+ end
108
+
109
+ context 'with non-seekable files like /dev/stdout (issue #150)' do
110
+ # Issue #150: Using w+ mode fails when writing to pipes because
111
+ # pipes are not seekable. We should use w mode (write-only) instead.
112
+ it 'should open files in write-only mode (w) not read+write mode (w+)' do
113
+ # Verify File.open is called with 'w' mode, not 'w+'
114
+ expect(File).to receive(:open).with(filename, 'w').and_call_original
115
+ SeedDump.dump(Sample, file: filename)
116
+ end
50
117
 
51
- File.open(@filename) { |file| file.read.should eq(expected_output + expected_output) }
118
+ it 'should open files in append mode (a) not read+append mode (a+)' do
119
+ # Verify File.open is called with 'a' mode, not 'a+'
120
+ expect(File).to receive(:open).with(filename, 'a').and_call_original
121
+ SeedDump.dump(Sample, file: filename, append: true)
52
122
  end
53
123
  end
54
124
  end
55
125
 
56
126
  context 'ActiveRecord relation' do
57
127
  it 'should return nil if the count is 0' do
58
- SeedDump.dump(EmptyModel).should be(nil)
128
+ expect(SeedDump.dump(EmptyModel)).to be_nil
59
129
  end
60
130
 
61
131
  context 'with an order parameter' do
132
+ before(:each) do
133
+ # Create samples with specific orderable values (0, 1, 2)
134
+ 3.times { |i| FactoryBot.create(:sample, integer: i) }
135
+ end
136
+
62
137
  it 'should dump the models in the specified order' do
63
- Sample.delete_all
64
- samples = 3.times {|i| FactoryBot.create(:sample, integer: i) }
138
+ # Define expected output based on descending integer order (2, 1, 0)
139
+ # Uses ISO 8601 format with timezone suffix (issue #111)
140
+ expected_desc_output = "Sample.create!([\n "
141
+ data = 2.downto(0).map do |i|
142
+ "{string: \"string\", text: \"text\", integer: #{i}, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04T19:14:00Z\", time: \"2000-01-01T03:15:00Z\", date: \"1863-11-19\", binary: \"binary\", boolean: false}"
143
+ end
144
+ expected_desc_output += data.join(",\n ") + "\n])\n"
65
145
 
66
- SeedDump.dump(Sample.order('integer DESC')).should eq("Sample.create!([\n {string: \"string\", text: \"text\", integer: 2, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04 19:14:00\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false},\n {string: \"string\", text: \"text\", integer: 1, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04 19:14:00\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false},\n {string: \"string\", text: \"text\", integer: 0, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04 19:14:00\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false}\n])\n")
146
+ expect(SeedDump.dump(Sample.order('integer DESC'))).to eq(expected_desc_output)
67
147
  end
68
148
  end
69
149
 
70
150
  context 'without an order parameter' do
151
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
71
152
  it 'should dump the models sorted by primary key ascending' do
72
- SeedDump.dump(Sample).should eq(expected_output)
153
+ expect(SeedDump.dump(Sample)).to eq(expected_output) # Expects 3 standard samples
73
154
  end
74
155
  end
75
156
 
76
157
  context 'with a limit parameter' do
77
158
  it 'should dump the number of models specified by the limit when the limit is smaller than the batch size' do
78
- expected_output = "Sample.create!([\n {string: \"string\", text: \"text\", integer: 42, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04 19:14:00\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false}\n])\n"
79
-
80
- SeedDump.dump(Sample.limit(1)).should eq(expected_output)
159
+ # Create one sample record (integer will be 42 from factory)
160
+ FactoryBot.create(:sample)
161
+ # Expected output for a single record, ISO 8601 format with timezone
162
+ expected_limit_1 = "Sample.create!([\n {string: \"string\", text: \"text\", integer: 42, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04T19:14:00Z\", time: \"2000-01-01T03:15:00Z\", date: \"1863-11-19\", binary: \"binary\", boolean: false}\n])\n"
163
+ expect(SeedDump.dump(Sample.limit(1))).to eq(expected_limit_1)
81
164
  end
82
165
 
83
166
  it 'should dump the number of models specified by the limit when the limit is larger than the batch size but not a multiple of the batch size' do
84
- Sample.delete_all
167
+ # Create 4 samples (integer will be 42 from factory)
85
168
  4.times { FactoryBot.create(:sample) }
169
+ # Expecting first 3 records with batch_size: 2 -> 2 create! calls
170
+ # First batch: 2 records, Second batch: 1 record
171
+ sample_data = "{string: \"string\", text: \"text\", integer: 42, float: 3.14, decimal: \"2.72\", datetime: \"1776-07-04T19:14:00Z\", time: \"2000-01-01T03:15:00Z\", date: \"1863-11-19\", binary: \"binary\", boolean: false}"
172
+ expected_limit_3 = "Sample.create!([\n #{sample_data},\n #{sample_data}\n])\n"
173
+ expected_limit_3 += "Sample.create!([\n #{sample_data}\n])\n"
86
174
 
87
- SeedDump.dump(Sample.limit(3), batch_size: 2).should eq(expected_output(false, 3))
175
+ expect(SeedDump.dump(Sample.limit(3), batch_size: 2)).to eq(expected_limit_3)
88
176
  end
89
177
  end
90
178
  end
91
179
 
92
180
  context 'with a batch_size parameter' do
181
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
93
182
  it 'should not raise an exception' do
94
- SeedDump.dump(Sample, batch_size: 100)
183
+ expect { SeedDump.dump(Sample, batch_size: 100) }.not_to raise_error
95
184
  end
96
185
 
97
186
  it 'should not cause records to not be dumped' do
98
- SeedDump.dump(Sample, batch_size: 2).should eq(expected_output)
187
+ expect(SeedDump.dump(Sample, batch_size: 2)).to include('string: "string"')
188
+ expect(SeedDump.dump(Sample, batch_size: 1)).to include('string: "string"')
189
+ end
190
+
191
+ it 'should output separate create! calls for each batch (issue #127)' do
192
+ result = SeedDump.dump(Sample, batch_size: 2)
193
+ # With 3 records and batch_size: 2, we should have 2 create! calls:
194
+ # - First batch with 2 records
195
+ # - Second batch with 1 record
196
+ expect(result.scan(/Sample\.create!\(/).count).to eq(2)
197
+ end
99
198
 
100
- SeedDump.dump(Sample, batch_size: 1).should eq(expected_output)
199
+ it 'should output all records in a single call when batch_size is larger than record count' do
200
+ result = SeedDump.dump(Sample, batch_size: 100)
201
+ # With 3 records and batch_size: 100, we should have 1 create! call
202
+ expect(result.scan(/Sample\.create!\(/).count).to eq(1)
203
+ end
204
+
205
+ it 'should output one create! call per record when batch_size is 1' do
206
+ result = SeedDump.dump(Sample, batch_size: 1)
207
+ # With 3 records and batch_size: 1, we should have 3 create! calls
208
+ expect(result.scan(/Sample\.create!\(/).count).to eq(3)
101
209
  end
102
210
  end
103
211
 
104
212
  context 'Array' do
213
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
105
214
  it 'should return the dump of the models passed in' do
106
- SeedDump.dump(Sample.all.to_a, batch_size: 2).should eq(expected_output)
215
+ # With batch_size: 2 and 3 records, we get 2 create! calls
216
+ result = SeedDump.dump(Sample.all.to_a, batch_size: 2)
217
+ expect(result).to include('Sample.create!')
218
+ expect(result.scan(/Sample\.create!\(/).count).to eq(2)
107
219
  end
108
220
 
109
221
  it 'should return nil if the array is empty' do
110
- SeedDump.dump([]).should be(nil)
222
+ expect(SeedDump.dump([])).to be_nil
111
223
  end
112
224
  end
113
225
 
114
226
  context 'with an exclude parameter' do
227
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
115
228
  it 'should exclude the specified attributes from the dump' do
116
- expected_output = "Sample.create!([\n {text: \"text\", integer: 42, decimal: \"2.72\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false},\n {text: \"text\", integer: 42, decimal: \"2.72\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false},\n {text: \"text\", integer: 42, decimal: \"2.72\", time: \"2000-01-01 03:15:00\", date: \"1863-11-19\", binary: \"binary\", boolean: false}\n])\n"
117
-
118
- SeedDump.dump(Sample, exclude: [:id, :created_at, :updated_at, :string, :float, :datetime]).should eq(expected_output)
229
+ # Uses ISO 8601 format with timezone suffix (issue #111)
230
+ expected_excluded_output = "Sample.create!([\n {text: \"text\", integer: 42, decimal: \"2.72\", time: \"2000-01-01T03:15:00Z\", date: \"1863-11-19\", binary: \"binary\", boolean: false},\n {text: \"text\", integer: 42, decimal: \"2.72\", time: \"2000-01-01T03:15:00Z\", date: \"1863-11-19\", binary: \"binary\", boolean: false},\n {text: \"text\", integer: 42, decimal: \"2.72\", time: \"2000-01-01T03:15:00Z\", date: \"1863-11-19\", binary: \"binary\", boolean: false}\n])\n"
231
+ expect(SeedDump.dump(Sample, exclude: [:id, :created_at, :updated_at, :string, :float, :datetime])).to eq(expected_excluded_output)
119
232
  end
120
233
  end
121
234
 
122
235
  context 'Range' do
123
- it 'should dump a class with ranges' do
124
- expected_output = "RangeSample.create!([\n {range_with_end_included: \"[1,3]\", range_with_end_excluded: \"[1,3)\", positive_infinite_range: \"[1,]\", negative_infinite_range: \"[,1]\", infinite_range: \"[,]\"}\n])\n"
236
+ let(:range_sample_mock) do
237
+ mock_class = Class.new do
238
+ def self.name; "RangeSample"; end
239
+ def self.<(other); other == ActiveRecord::Base; end
240
+ def attributes
241
+ {
242
+ "range_with_end_included" => (1..3),
243
+ "range_with_end_excluded" => (1...3),
244
+ "positive_infinite_range" => (1..Float::INFINITY),
245
+ "negative_infinite_range" => (-Float::INFINITY..1),
246
+ "infinite_range" => (-Float::INFINITY..Float::INFINITY)
247
+ }
248
+ end
249
+ def attribute_names; attributes.keys; end
250
+ end
251
+ Object.const_set("RangeSample", mock_class) unless defined?(RangeSample)
252
+ RangeSample.new
253
+ end
125
254
 
126
- SeedDump.dump([RangeSample.new]).should eq(expected_output)
255
+ it 'should dump an object with ranges' do
256
+ expected_range_output = "RangeSample.create!([\n {range_with_end_included: \"[1,3]\", range_with_end_excluded: \"[1,3)\", positive_infinite_range: \"[1,]\", negative_infinite_range: \"[,1]\", infinite_range: \"[,]\"}\n])\n"
257
+ expect(SeedDump.dump([range_sample_mock])).to eq(expected_range_output)
258
+ end
259
+ end
260
+
261
+ context 'ActionText::Content (issue #154)' do
262
+ # Mock ActionText::Content class to simulate ActionText behavior
263
+ before(:all) do
264
+ unless defined?(ActionText::Content)
265
+ module ActionText
266
+ class Content
267
+ def initialize(html)
268
+ @html = html
269
+ end
270
+
271
+ def to_s
272
+ @html
273
+ end
274
+
275
+ def inspect
276
+ "#<ActionText::Content \"#{@html[0..20]}...\">"
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ let(:action_text_sample_mock) do
284
+ mock_class = Class.new do
285
+ def self.name; "ActionTextSample"; end
286
+ def self.<(other); other == ActiveRecord::Base; end
287
+ def is_a?(klass)
288
+ return true if klass == ActiveRecord::Base
289
+ super
290
+ end
291
+ def class
292
+ ActionTextSample
293
+ end
294
+ def attributes
295
+ {
296
+ "name" => "article",
297
+ "body" => ActionText::Content.new("<div>Hello <strong>World</strong></div>")
298
+ }
299
+ end
300
+ def attribute_names; attributes.keys; end
301
+ end
302
+ Object.const_set("ActionTextSample", mock_class) unless defined?(ActionTextSample)
303
+ ActionTextSample.new
304
+ end
305
+
306
+ it 'should dump ActionText::Content as its HTML string representation' do
307
+ result = SeedDump.dump([action_text_sample_mock], exclude: [])
308
+ expect(result).to include('body: "<div>Hello <strong>World</strong></div>"')
309
+ expect(result).not_to include('#<ActionText::Content')
310
+ end
311
+ end
312
+
313
+ context 'table without primary key (issue #167)' do
314
+ before(:each) do
315
+ CampaignsManager.create!(campaign_id: 1, manager_id: 1)
316
+ CampaignsManager.create!(campaign_id: 2, manager_id: 2)
317
+ end
318
+
319
+ it 'should dump records without raising an error' do
320
+ expect { SeedDump.dump(CampaignsManager) }.not_to raise_error
321
+ end
322
+
323
+ it 'should return the dump of the models' do
324
+ result = SeedDump.dump(CampaignsManager, exclude: [])
325
+ expect(result).to include('CampaignsManager.create!')
326
+ expect(result).to include('campaign_id: 1')
327
+ expect(result).to include('campaign_id: 2')
328
+ end
329
+ end
330
+
331
+ context 'model with default_scope using select (issue #165)' do
332
+ before(:each) do
333
+ ScopedSelectSample.unscoped.create!(name: 'test1', description: 'desc1')
334
+ ScopedSelectSample.unscoped.create!(name: 'test2', description: 'desc2')
335
+ end
336
+
337
+ it 'should dump records without raising a COUNT error' do
338
+ expect { SeedDump.dump(ScopedSelectSample) }.not_to raise_error
339
+ end
340
+
341
+ it 'should return the dump of the models' do
342
+ result = SeedDump.dump(ScopedSelectSample)
343
+ expect(result).to include('ScopedSelectSample.create!')
344
+ expect(result).to include('name: "test1"')
345
+ expect(result).to include('name: "test2"')
127
346
  end
128
347
  end
129
348
 
130
349
  context 'activerecord-import' do
350
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
131
351
  it 'should dump in the activerecord-import format when import is true' do
132
- SeedDump.dump(Sample, import: true, exclude: []).should eq <<-RUBY
133
- Sample.import([:id, :string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean, :created_at, :updated_at], [
134
- [1, "string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false, "1969-07-20 20:18:00", "1989-11-10 04:20:00"],
135
- [2, "string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false, "1969-07-20 20:18:00", "1989-11-10 04:20:00"],
136
- [3, "string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false, "1969-07-20 20:18:00", "1989-11-10 04:20:00"]
137
- ])
138
- RUBY
352
+ expect(SeedDump.dump(Sample, import: true, exclude: [])).to eq(expected_import_output(false))
139
353
  end
140
354
 
141
355
  it 'should omit excluded columns if they are specified' do
142
- SeedDump.dump(Sample, import: true, exclude: [:id, :created_at, :updated_at]).should eq <<-RUBY
143
- Sample.import([:string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean], [
144
- ["string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false],
145
- ["string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false],
146
- ["string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false]
147
- ])
148
- RUBY
356
+ expect(SeedDump.dump(Sample, import: true, exclude: [:id, :created_at, :updated_at])).to eq(expected_import_output(true))
149
357
  end
150
358
 
151
359
  context 'should add the params to the output if they are specified' do
152
360
  it 'should dump in the activerecord-import format when import is true' do
153
- SeedDump.dump(Sample, import: { validate: false }, exclude: []).should eq <<-RUBY
154
- Sample.import([:id, :string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean, :created_at, :updated_at], [
155
- [1, "string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false, "1969-07-20 20:18:00", "1989-11-10 04:20:00"],
156
- [2, "string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false, "1969-07-20 20:18:00", "1989-11-10 04:20:00"],
157
- [3, "string", "text", 42, 3.14, "2.72", "1776-07-04 19:14:00", "2000-01-01 03:15:00", "1863-11-19", "binary", false, "1969-07-20 20:18:00", "1989-11-10 04:20:00"]
158
- ], validate: false)
159
- RUBY
361
+ expect(SeedDump.dump(Sample, import: { validate: false }, exclude: [])).to eq(expected_import_output_with_options)
160
362
  end
161
363
  end
162
364
  end
163
- end
164
- end
165
365
 
166
- class RangeSample
167
- def attributes
168
- {
169
- "range_with_end_included" => (1..3),
170
- "range_with_end_excluded" => (1...3),
171
- "positive_infinite_range" => (1..Float::INFINITY),
172
- "negative_infinite_range" => (-Float::INFINITY..1),
173
- "infinite_range" => (-Float::INFINITY..Float::INFINITY)
174
- }
366
+ context 'insert_all (issue #153)' do
367
+ before(:each) { FactoryBot.create_list(:sample, 3) } # Create 3 standard samples
368
+
369
+ it 'should dump in the insert_all format when insert_all option is true' do
370
+ expect(SeedDump.dump(Sample, insert_all: true)).to eq(expected_insert_all_output(true))
371
+ end
372
+
373
+ it 'should include all columns when exclude is empty' do
374
+ expect(SeedDump.dump(Sample, insert_all: true, exclude: [])).to eq(expected_insert_all_output(false))
375
+ end
376
+
377
+ it 'should use Hash syntax (not Array syntax like activerecord-import)' do
378
+ result = SeedDump.dump(Sample, insert_all: true)
379
+ # insert_all uses Hash format: {key: value, ...}
380
+ expect(result).to include('string: "string"')
381
+ expect(result).not_to include('[:string') # Not array format
382
+ end
383
+
384
+ it 'should not include column names header like activerecord-import does' do
385
+ result = SeedDump.dump(Sample, insert_all: true)
386
+ # activerecord-import format includes: Model.import([:col1, :col2], [...])
387
+ # insert_all format is just: Model.insert_all([{...}, {...}])
388
+ expect(result).not_to match(/insert_all\(\[:\w+/)
389
+ end
390
+ end
391
+
392
+ context 'upsert_all (issue #104 - non-continuous IDs / foreign key preservation)' do
393
+ # Issue #104: When rows are deleted from a parent table, re-importing seeds
394
+ # can result in foreign key references pointing to wrong records because
395
+ # auto-increment IDs change. upsert_all solves this by preserving original IDs.
396
+ #
397
+ # upsert_all uses Rails 6+ upsert_all which can set specific IDs, so foreign
398
+ # key references remain valid after reimport.
399
+ before(:each) { FactoryBot.create_list(:sample, 3) }
400
+
401
+ it 'should dump in the upsert_all format when upsert_all option is true' do
402
+ result = SeedDump.dump(Sample, upsert_all: true)
403
+ expect(result).to include('Sample.upsert_all(')
404
+ expect(result).to include('id: 1')
405
+ expect(result).to include('id: 2')
406
+ expect(result).to include('id: 3')
407
+ end
408
+
409
+ it 'should include id column by default (unlike other dump modes)' do
410
+ # upsert_all needs IDs to preserve foreign key relationships
411
+ result = SeedDump.dump(Sample, upsert_all: true)
412
+ expect(result).to include('id: 1')
413
+ end
414
+
415
+ it 'should still exclude created_at and updated_at by default' do
416
+ result = SeedDump.dump(Sample, upsert_all: true)
417
+ expect(result).not_to include('created_at')
418
+ expect(result).not_to include('updated_at')
419
+ end
420
+
421
+ it 'should use Hash syntax like insert_all' do
422
+ result = SeedDump.dump(Sample, upsert_all: true)
423
+ expect(result).to include('string: "string"')
424
+ expect(result).not_to include('[:string')
425
+ end
426
+
427
+ it 'should handle custom exclude that includes :id' do
428
+ # If user explicitly excludes :id, respect that
429
+ result = SeedDump.dump(Sample, upsert_all: true, exclude: [:id, :created_at, :updated_at])
430
+ expect(result).not_to include('id:')
431
+ end
432
+
433
+ it 'should output separate upsert_all calls for each batch' do
434
+ result = SeedDump.dump(Sample, upsert_all: true, batch_size: 2)
435
+ expect(result.scan(/Sample\.upsert_all\(/).count).to eq(2)
436
+ end
437
+
438
+ context 'with foreign key relationships' do
439
+ # The main use case: preserving foreign key relationships across reimports
440
+ before(:each) do
441
+ # Clear the automatically created samples from the outer before block
442
+ Sample.delete_all
443
+
444
+ # Create authors with specific IDs that might have gaps
445
+ author1 = Author.create!(name: 'First Author')
446
+ author2 = Author.create!(name: 'Second Author')
447
+
448
+ # Create books referencing these authors
449
+ Book.create!(title: 'Book by First', author: author1)
450
+ Book.create!(title: 'Book by Second', author: author2)
451
+ end
452
+
453
+ it 'should preserve author IDs so book foreign keys remain valid' do
454
+ result = SeedDump.dump(Author, upsert_all: true)
455
+ expect(result).to include('id: 1')
456
+ expect(result).to include('id: 2')
457
+ expect(result).to include('upsert_all')
458
+ end
459
+
460
+ it 'should preserve foreign key references in child records' do
461
+ result = SeedDump.dump(Book, upsert_all: true)
462
+ expect(result).to include('author_id: 1')
463
+ expect(result).to include('author_id: 2')
464
+ end
465
+ end
466
+ end
467
+
468
+ context 'HABTM join models (issue #130)' do
469
+ # Rails creates private constants like `Model::HABTM_OtherModels` for
470
+ # has_and_belongs_to_many associations. These cannot be referenced directly
471
+ # in seeds.rb because they're private. We need to use const_get instead.
472
+ #
473
+ # Instead of: Dealer::HABTM_UStations.create!([...])
474
+ # We output: Dealer.const_get('HABTM_UStations').create!([...])
475
+
476
+ let(:habtm_mock_class) do
477
+ Class.new do
478
+ def self.name; "Dealer::HABTM_UStations"; end
479
+ def self.<(other); other == ActiveRecord::Base; end
480
+ def self.to_s; name; end
481
+ end
482
+ end
483
+
484
+ let(:habtm_mock) do
485
+ klass = habtm_mock_class
486
+ mock_instance = Object.new
487
+ mock_instance.define_singleton_method(:class) { klass }
488
+ mock_instance.define_singleton_method(:is_a?) do |other|
489
+ other == ActiveRecord::Base || super(other)
490
+ end
491
+ mock_instance.define_singleton_method(:attributes) do
492
+ { "dealer_id" => 1, "ustation_id" => 2 }
493
+ end
494
+ mock_instance.define_singleton_method(:attribute_names) do
495
+ ["dealer_id", "ustation_id"]
496
+ end
497
+ mock_instance
498
+ end
499
+
500
+ it 'should output const_get format for HABTM models' do
501
+ result = SeedDump.dump([habtm_mock], exclude: [])
502
+ # Should use const_get to access the private constant
503
+ expect(result).to include("Dealer.const_get('HABTM_UStations').create!")
504
+ expect(result).not_to include("Dealer::HABTM_UStations.create!")
505
+ end
506
+
507
+ it 'should include the record data' do
508
+ result = SeedDump.dump([habtm_mock], exclude: [])
509
+ expect(result).to include("dealer_id: 1")
510
+ expect(result).to include("ustation_id: 2")
511
+ end
512
+
513
+ it 'should produce output that can be evaluated without NameError' do
514
+ # Create a class structure with private constant to test const_get works
515
+ # Use a plain Ruby class (not ActiveRecord) to avoid polluting AR.descendants
516
+ test_parent = Class.new
517
+ Object.const_set('TestDealerParent', test_parent)
518
+
519
+ habtm_class = Class.new
520
+ TestDealerParent.const_set('HABTM_Stations', habtm_class)
521
+ TestDealerParent.send(:private_constant, 'HABTM_Stations')
522
+
523
+ begin
524
+ # Verify that const_get can access the private constant
525
+ resolved_class = TestDealerParent.const_get('HABTM_Stations')
526
+ expect(resolved_class).to eq(habtm_class)
527
+
528
+ # Verify that direct reference WOULD fail (proving we need const_get)
529
+ # Error message varies by Ruby/Rails version:
530
+ # - "private constant" in newer versions
531
+ # - "uninitialized constant" in older versions (private constants appear uninitialized)
532
+ expect { eval("TestDealerParent::HABTM_Stations") }.to raise_error(NameError)
533
+
534
+ # Now test that our dump output format works with the mock
535
+ result = SeedDump.dump([habtm_mock], exclude: [])
536
+ expect(result).to include("Dealer.const_get('HABTM_UStations').create!")
537
+
538
+ # Verify the generated model reference pattern is syntactically valid Ruby
539
+ # that would resolve correctly (we can't actually eval it without Dealer existing)
540
+ expect(result).to match(/\w+\.const_get\('\w+'\)\.create!/)
541
+ ensure
542
+ TestDealerParent.send(:remove_const, 'HABTM_Stations') if TestDealerParent.const_defined?('HABTM_Stations', false)
543
+ Object.send(:remove_const, 'TestDealerParent') if defined?(TestDealerParent)
544
+ end
545
+ end
546
+
547
+ context 'with nested namespace' do
548
+ let(:nested_habtm_mock_class) do
549
+ Class.new do
550
+ def self.name; "Admin::Dealers::Dealer::HABTM_UStations"; end
551
+ def self.<(other); other == ActiveRecord::Base; end
552
+ def self.to_s; name; end
553
+ end
554
+ end
555
+
556
+ let(:nested_habtm_mock) do
557
+ klass = nested_habtm_mock_class
558
+ mock_instance = Object.new
559
+ mock_instance.define_singleton_method(:class) { klass }
560
+ mock_instance.define_singleton_method(:is_a?) do |other|
561
+ other == ActiveRecord::Base || super(other)
562
+ end
563
+ mock_instance.define_singleton_method(:attributes) do
564
+ { "dealer_id" => 1, "ustation_id" => 2 }
565
+ end
566
+ mock_instance.define_singleton_method(:attribute_names) do
567
+ ["dealer_id", "ustation_id"]
568
+ end
569
+ mock_instance
570
+ end
571
+
572
+ it 'should handle deeply nested namespaces' do
573
+ result = SeedDump.dump([nested_habtm_mock], exclude: [])
574
+ expect(result).to include("Admin::Dealers::Dealer.const_get('HABTM_UStations').create!")
575
+ end
576
+ end
577
+ end
578
+
579
+ context 'serialized Hash in text field (issue #105)' do
580
+ it 'should dump serialized fields as valid Ruby that can be loaded' do
581
+ SerializedSample.create!(
582
+ name: 'test',
583
+ metadata: { 'key' => 'value', 'number' => 42, 'nested' => { 'a' => 1 } }
584
+ )
585
+ result = SeedDump.dump(SerializedSample)
586
+ expect(result).to include('SerializedSample.create!')
587
+ expect(result).to include('name: "test"')
588
+
589
+ # The metadata field should be dumped as a valid Ruby Hash literal
590
+ # Not as the raw JSON string or malformed output
591
+ # Ruby's Hash#inspect uses ' => ' with spaces
592
+ expect(result).to include('metadata: {"key" => "value"')
593
+ expect(result).to include('"number" => 42')
594
+ expect(result).to include('"nested" => {"a" => 1}')
595
+ end
596
+
597
+ it 'should produce output that can be evaluated as valid Ruby' do
598
+ SerializedSample.create!(
599
+ name: 'test',
600
+ metadata: { 'key' => 'value', 'number' => 42, 'nested' => { 'a' => 1 } }
601
+ )
602
+ result = SeedDump.dump(SerializedSample)
603
+ # The dump should produce valid Ruby syntax
604
+ expect { eval(result) rescue NameError }.not_to raise_error
605
+ end
606
+
607
+ it 'should handle DateTime objects in serialized Hashes as ISO 8601 strings' do
608
+ # The original issue #105 was about DateTime objects inside serialized Hashes
609
+ # being output as unquoted datetime objects like: 2016-05-25 17:00:00 UTC
610
+ # which isn't valid Ruby syntax. With JSON serialization, Rails stores these
611
+ # as ISO 8601 strings in the database, which should be dumped correctly.
612
+ SerializedSample.create!(
613
+ name: 'audit_log',
614
+ metadata: {
615
+ 'event' => 'update',
616
+ 'changed_at' => Time.utc(2016, 5, 25, 17, 0, 0).iso8601,
617
+ 'changes' => { 'status' => ['pending', 'completed'] }
618
+ }
619
+ )
620
+ result = SeedDump.dump(SerializedSample)
621
+
622
+ # Should include the datetime as a quoted string
623
+ expect(result).to include('"changed_at" => "2016-05-25T17:00:00Z"')
624
+ # The output should be valid Ruby
625
+ expect { eval(result) rescue NameError }.not_to raise_error
626
+ end
627
+
628
+ context 'with Time objects nested in Hashes' do
629
+ # This tests the core issue #105: Time objects inside Hashes produce
630
+ # invalid Ruby when .inspect is called on the Hash.
631
+ # e.g. {"changed_at" => 2016-05-25 17:00:00 UTC} is not valid Ruby
632
+ let(:hash_with_time_mock) do
633
+ mock_class = Class.new do
634
+ def self.name; "HashWithTimeSample"; end
635
+ def self.<(other); other == ActiveRecord::Base; end
636
+ def is_a?(klass)
637
+ return true if klass == ActiveRecord::Base
638
+ super
639
+ end
640
+ def class
641
+ HashWithTimeSample
642
+ end
643
+ def attributes
644
+ {
645
+ "name" => "audit_log",
646
+ # This Hash contains actual Time objects, which would be
647
+ # the case with YAML-serialized fields in older Rails
648
+ "metadata" => {
649
+ "event" => "update",
650
+ "changed_at" => Time.utc(2016, 5, 25, 17, 0, 0),
651
+ "changes" => { "status" => ["pending", "completed"] }
652
+ }
653
+ }
654
+ end
655
+ def attribute_names; attributes.keys; end
656
+ end
657
+ Object.const_set("HashWithTimeSample", mock_class) unless defined?(HashWithTimeSample)
658
+ HashWithTimeSample.new
659
+ end
660
+
661
+ it 'should produce valid Ruby when Hash contains Time objects' do
662
+ result = SeedDump.dump([hash_with_time_mock], exclude: [])
663
+
664
+ # The output should be valid Ruby syntax - this is the core bug
665
+ # Without the fix, this produces: metadata: {"changed_at" => 2016-05-25 17:00:00 UTC}
666
+ # which is a SyntaxError
667
+ expect { eval(result) rescue NameError }.not_to raise_error
668
+ end
669
+
670
+ it 'should convert Time objects inside Hashes to ISO 8601 format' do
671
+ result = SeedDump.dump([hash_with_time_mock], exclude: [])
672
+
673
+ # Time objects should be converted to ISO 8601 strings
674
+ expect(result).to match(/"changed_at" => "2016-05-25T17:00:00(\+00:00|Z)"/)
675
+ end
676
+ end
677
+
678
+ context 'with BigDecimal objects nested in Hashes' do
679
+ let(:hash_with_bigdecimal_mock) do
680
+ mock_class = Class.new do
681
+ def self.name; "HashWithBigDecimalSample"; end
682
+ def self.<(other); other == ActiveRecord::Base; end
683
+ def is_a?(klass)
684
+ return true if klass == ActiveRecord::Base
685
+ super
686
+ end
687
+ def class
688
+ HashWithBigDecimalSample
689
+ end
690
+ def attributes
691
+ {
692
+ "name" => "pricing",
693
+ "data" => {
694
+ "price" => BigDecimal("19.99"),
695
+ "tax_rate" => BigDecimal("0.08"),
696
+ "nested" => { "discount" => BigDecimal("5.00") }
697
+ }
698
+ }
699
+ end
700
+ def attribute_names; attributes.keys; end
701
+ end
702
+ Object.const_set("HashWithBigDecimalSample", mock_class) unless defined?(HashWithBigDecimalSample)
703
+ HashWithBigDecimalSample.new
704
+ end
705
+
706
+ it 'should produce valid Ruby when Hash contains BigDecimal objects' do
707
+ result = SeedDump.dump([hash_with_bigdecimal_mock], exclude: [])
708
+
709
+ # The output should be valid Ruby syntax
710
+ expect { eval(result) rescue NameError }.not_to raise_error
711
+ end
712
+
713
+ it 'should convert BigDecimal objects inside Hashes to string format' do
714
+ result = SeedDump.dump([hash_with_bigdecimal_mock], exclude: [])
715
+
716
+ # BigDecimal objects should be converted to strings
717
+ expect(result).to include('"price" => "19.99"')
718
+ expect(result).to include('"tax_rate" => "0.08"')
719
+ expect(result).to include('"discount" => "5.0"')
720
+ end
721
+ end
722
+
723
+ context 'with mixed types nested in Arrays' do
724
+ let(:array_with_mixed_types_mock) do
725
+ mock_class = Class.new do
726
+ def self.name; "ArrayWithMixedTypesSample"; end
727
+ def self.<(other); other == ActiveRecord::Base; end
728
+ def is_a?(klass)
729
+ return true if klass == ActiveRecord::Base
730
+ super
731
+ end
732
+ def class
733
+ ArrayWithMixedTypesSample
734
+ end
735
+ def attributes
736
+ {
737
+ "name" => "events",
738
+ "timestamps" => [
739
+ Time.utc(2016, 1, 1, 0, 0, 0),
740
+ Time.utc(2016, 6, 15, 12, 30, 0),
741
+ Time.utc(2016, 12, 31, 23, 59, 59)
742
+ ],
743
+ "prices" => [
744
+ BigDecimal("10.00"),
745
+ BigDecimal("20.50"),
746
+ BigDecimal("30.99")
747
+ ]
748
+ }
749
+ end
750
+ def attribute_names; attributes.keys; end
751
+ end
752
+ Object.const_set("ArrayWithMixedTypesSample", mock_class) unless defined?(ArrayWithMixedTypesSample)
753
+ ArrayWithMixedTypesSample.new
754
+ end
755
+
756
+ it 'should produce valid Ruby when Array contains Time/BigDecimal objects' do
757
+ result = SeedDump.dump([array_with_mixed_types_mock], exclude: [])
758
+
759
+ # The output should be valid Ruby syntax
760
+ expect { eval(result) rescue NameError }.not_to raise_error
761
+ end
762
+
763
+ it 'should convert Time objects inside Arrays to ISO 8601 format' do
764
+ result = SeedDump.dump([array_with_mixed_types_mock], exclude: [])
765
+
766
+ expect(result).to include('"2016-01-01T00:00:00Z"')
767
+ expect(result).to include('"2016-06-15T12:30:00Z"')
768
+ expect(result).to include('"2016-12-31T23:59:59Z"')
769
+ end
770
+
771
+ it 'should convert BigDecimal objects inside Arrays to string format' do
772
+ result = SeedDump.dump([array_with_mixed_types_mock], exclude: [])
773
+
774
+ expect(result).to include('"10.0"')
775
+ expect(result).to include('"20.5"')
776
+ expect(result).to include('"30.99"')
777
+ end
778
+ end
779
+ end
780
+
781
+ context 'DateTime timezone preservation (issue #111)' do
782
+ let(:datetime_sample_mock) do
783
+ mock_class = Class.new do
784
+ def self.name; "DateTimeSample"; end
785
+ def self.<(other); other == ActiveRecord::Base; end
786
+ def is_a?(klass)
787
+ return true if klass == ActiveRecord::Base
788
+ super
789
+ end
790
+ def class
791
+ DateTimeSample
792
+ end
793
+ def attributes
794
+ {
795
+ "name" => "test",
796
+ # UTC datetime - should preserve timezone info in dump
797
+ "scheduled_at" => Time.utc(2016, 8, 12, 2, 20, 20)
798
+ }
799
+ end
800
+ def attribute_names; attributes.keys; end
801
+ end
802
+ Object.const_set("DateTimeSample", mock_class) unless defined?(DateTimeSample)
803
+ DateTimeSample.new
804
+ end
805
+
806
+ it 'should include timezone information in datetime dumps' do
807
+ result = SeedDump.dump([datetime_sample_mock], exclude: [])
808
+ # The datetime should include timezone info (UTC) so it can be reimported correctly
809
+ # Format should be ISO 8601: "2016-08-12T02:20:20Z" or similar with timezone
810
+ expect(result).to match(/scheduled_at: "2016-08-12T02:20:20(\+00:00|Z)"/)
811
+ end
812
+
813
+ it 'should preserve non-UTC timezone information' do
814
+ # Create a mock with a non-UTC timezone
815
+ non_utc_mock_class = Class.new do
816
+ def self.name; "NonUtcSample"; end
817
+ def self.<(other); other == ActiveRecord::Base; end
818
+ def is_a?(klass)
819
+ return true if klass == ActiveRecord::Base
820
+ super
821
+ end
822
+ def class
823
+ NonUtcSample
824
+ end
825
+ def attributes
826
+ {
827
+ "name" => "test",
828
+ # Pacific time (-08:00)
829
+ "scheduled_at" => Time.new(2016, 8, 12, 2, 20, 20, "-08:00")
830
+ }
831
+ end
832
+ def attribute_names; attributes.keys; end
833
+ end
834
+ Object.const_set("NonUtcSample", non_utc_mock_class) unless defined?(NonUtcSample)
835
+ non_utc_sample = NonUtcSample.new
836
+
837
+ result = SeedDump.dump([non_utc_sample], exclude: [])
838
+ # Should include the timezone offset
839
+ expect(result).to match(/scheduled_at: "2016-08-12T02:20:20-08:00"/)
840
+ end
841
+ end
842
+
843
+ context 'CarrierWave uploader columns (issue #117)' do
844
+ # CarrierWave mounts uploaders on models which override the attribute getter.
845
+ # When record.attributes is called, it may return nil or an uploader object
846
+ # instead of the raw filename string. We need to detect this and extract the identifier.
847
+ #
848
+ # The issue reports that CarrierWave columns "always dump to 'nil'" - this happens
849
+ # because record.attributes bypasses the CarrierWave getter and returns the raw
850
+ # @attributes value, which may be nil even when the uploader has a file.
851
+
852
+ before(:all) do
853
+ # Mock CarrierWave::Uploader::Base if not already defined
854
+ unless defined?(CarrierWave::Uploader::Base)
855
+ module CarrierWave
856
+ module Uploader
857
+ class Base
858
+ attr_reader :identifier
859
+
860
+ def initialize(identifier)
861
+ @identifier = identifier
862
+ end
863
+
864
+ def inspect
865
+ "#<CarrierWave::Uploader::Base identifier=#{@identifier.inspect}>"
866
+ end
867
+
868
+ def to_s
869
+ # CarrierWave's to_s returns the URL, not the identifier
870
+ "/uploads/#{@identifier}"
871
+ end
872
+ end
873
+ end
874
+ end
875
+ end
876
+ end
877
+
878
+ context 'when record.attributes returns nil but getter returns uploader (the reported bug)' do
879
+ # This is the actual bug reported in issue #117:
880
+ # record.attributes['avatar'] returns nil, but record.avatar returns an uploader
881
+ # with an identifier. We need to call the getter to get the real value.
882
+ let(:nil_attributes_mock) do
883
+ uploader = CarrierWave::Uploader::Base.new("avatar123.jpg")
884
+ mock_class = Class.new do
885
+ def self.name; "NilAttributesSample"; end
886
+ def self.<(other); other == ActiveRecord::Base; end
887
+ def is_a?(klass)
888
+ return true if klass == ActiveRecord::Base
889
+ super
890
+ end
891
+ def class
892
+ NilAttributesSample
893
+ end
894
+ end
895
+
896
+ Object.const_set("NilAttributesSample", mock_class) unless defined?(NilAttributesSample)
897
+ instance = NilAttributesSample.new
898
+
899
+ # record.attributes returns nil for the avatar column
900
+ instance.define_singleton_method(:attributes) do
901
+ { "name" => "user1", "avatar" => nil }
902
+ end
903
+ instance.define_singleton_method(:attribute_names) { ["name", "avatar"] }
904
+
905
+ # But record.avatar returns the uploader with the actual filename
906
+ instance.define_singleton_method(:avatar) { uploader }
907
+
908
+ instance
909
+ end
910
+
911
+ it 'should dump the uploader identifier even when attributes returns nil' do
912
+ result = SeedDump.dump([nil_attributes_mock], exclude: [])
913
+ # Should include the filename from the uploader, not nil
914
+ expect(result).to include('avatar: "avatar123.jpg"')
915
+ expect(result).not_to include('avatar: nil')
916
+ end
917
+
918
+ it 'should produce valid Ruby' do
919
+ result = SeedDump.dump([nil_attributes_mock], exclude: [])
920
+ expect { eval(result) rescue NameError }.not_to raise_error
921
+ end
922
+ end
923
+
924
+ context 'when record.attributes returns an uploader object directly' do
925
+ let(:uploader_in_attributes_mock) do
926
+ mock_class = Class.new do
927
+ def self.name; "UploaderInAttributesSample"; end
928
+ def self.<(other); other == ActiveRecord::Base; end
929
+ def is_a?(klass)
930
+ return true if klass == ActiveRecord::Base
931
+ super
932
+ end
933
+ def class
934
+ UploaderInAttributesSample
935
+ end
936
+ def attributes
937
+ {
938
+ "name" => "user1",
939
+ # CarrierWave uploader object in the attributes hash
940
+ "avatar" => CarrierWave::Uploader::Base.new("avatar456.jpg")
941
+ }
942
+ end
943
+ def attribute_names; attributes.keys; end
944
+ end
945
+ Object.const_set("UploaderInAttributesSample", mock_class) unless defined?(UploaderInAttributesSample)
946
+ UploaderInAttributesSample.new
947
+ end
948
+
949
+ it 'should dump CarrierWave uploader columns as the identifier string' do
950
+ result = SeedDump.dump([uploader_in_attributes_mock], exclude: [])
951
+ # Should include the filename, not the uploader object's inspect output
952
+ expect(result).to include('avatar: "avatar456.jpg"')
953
+ expect(result).not_to include('#<CarrierWave')
954
+ expect(result).not_to include('/uploads/')
955
+ end
956
+ end
957
+
958
+ context 'with no file uploaded (nil identifier)' do
959
+ let(:no_file_mock) do
960
+ uploader = CarrierWave::Uploader::Base.new(nil)
961
+ mock_class = Class.new do
962
+ def self.name; "NoFileSample"; end
963
+ def self.<(other); other == ActiveRecord::Base; end
964
+ def is_a?(klass)
965
+ return true if klass == ActiveRecord::Base
966
+ super
967
+ end
968
+ def class
969
+ NoFileSample
970
+ end
971
+ end
972
+ Object.const_set("NoFileSample", mock_class) unless defined?(NoFileSample)
973
+ instance = NoFileSample.new
974
+ instance.define_singleton_method(:attributes) do
975
+ { "name" => "user2", "avatar" => nil }
976
+ end
977
+ instance.define_singleton_method(:attribute_names) { ["name", "avatar"] }
978
+ instance.define_singleton_method(:avatar) { uploader }
979
+ instance
980
+ end
981
+
982
+ it 'should handle CarrierWave uploaders with nil identifier' do
983
+ result = SeedDump.dump([no_file_mock], exclude: [])
984
+ expect(result).to include('avatar: nil')
985
+ expect(result).not_to include('#<CarrierWave')
986
+ end
987
+ end
988
+ end
989
+
990
+ context 'comment header (issue #126)' do
991
+ # Issue #126: Add a comment header to the seed file showing that seed_dump
992
+ # was used and what options were specified. This helps with traceability
993
+ # and understanding how the seed file was generated.
994
+
995
+ let(:tempfile) { Tempfile.new(['seed_dump_test', '.rb']) }
996
+ let(:filename) { tempfile.path }
997
+
998
+ before(:each) { FactoryBot.create_list(:sample, 2) }
999
+
1000
+ after do
1001
+ tempfile.close
1002
+ tempfile.unlink
1003
+ end
1004
+
1005
+ it 'should add a comment header when header option is true' do
1006
+ SeedDump.dump(Sample, file: filename, header: true)
1007
+ content = File.read(filename)
1008
+ expect(content).to start_with('# ')
1009
+ expect(content).to include('Generated by seed_dump')
1010
+ end
1011
+
1012
+ it 'should include a timestamp in the header' do
1013
+ SeedDump.dump(Sample, file: filename, header: true)
1014
+ content = File.read(filename)
1015
+ # Should include date in some format (YYYY-MM-DD)
1016
+ expect(content).to match(/\d{4}-\d{2}-\d{2}/)
1017
+ end
1018
+
1019
+ it 'should include a copyable rake command' do
1020
+ SeedDump.dump(Sample, file: filename, header: true, exclude: [:id, :created_at], batch_size: 100)
1021
+ content = File.read(filename)
1022
+ expect(content).to include('Rake command:')
1023
+ expect(content).to include('rake db:seed:dump')
1024
+ expect(content).to include('EXCLUDE=id,created_at')
1025
+ expect(content).to include('BATCH_SIZE=100')
1026
+ expect(content).to include('HEADER=true')
1027
+ end
1028
+
1029
+ it 'should include a copyable programmatic equivalent' do
1030
+ SeedDump.dump(Sample, file: filename, header: true, exclude: [:id, :created_at], batch_size: 100)
1031
+ content = File.read(filename)
1032
+ expect(content).to include('Programmatic equivalent:')
1033
+ expect(content).to include('SeedDump.dump(ModelName,')
1034
+ expect(content).to include('exclude: [:id, :created_at]')
1035
+ expect(content).to include('batch_size: 100')
1036
+ end
1037
+
1038
+ it 'should not add header when header option is false or not set' do
1039
+ SeedDump.dump(Sample, file: filename)
1040
+ content = File.read(filename)
1041
+ expect(content).not_to start_with('# ')
1042
+ expect(content).to start_with('Sample.create!')
1043
+ end
1044
+
1045
+ it 'should not add header when appending' do
1046
+ SeedDump.dump(Sample, file: filename, header: true)
1047
+ SeedDump.dump(Sample, file: filename, header: true, append: true)
1048
+ content = File.read(filename)
1049
+ # Should only have one header at the top
1050
+ expect(content.scan(/Generated by seed_dump/).count).to eq(1)
1051
+ end
1052
+
1053
+ it 'should not add header when returning a string (no file option)' do
1054
+ result = SeedDump.dump(Sample, header: true)
1055
+ expect(result).not_to include('Generated by seed_dump')
1056
+ expect(result).to start_with('Sample.create!')
1057
+ end
1058
+
1059
+ it 'should include FILE in rake command when non-default' do
1060
+ custom_file = Tempfile.new(['custom_seeds', '.rb'])
1061
+ begin
1062
+ SeedDump.dump(Sample, file: custom_file.path, header: true)
1063
+ content = File.read(custom_file.path)
1064
+ expect(content).to include("FILE=#{custom_file.path}")
1065
+ ensure
1066
+ custom_file.close
1067
+ custom_file.unlink
1068
+ end
1069
+ end
1070
+
1071
+ it 'should not include FILE in rake command when using default db/seeds.rb' do
1072
+ SeedDump.dump(Sample, file: filename, header: true)
1073
+ content = File.read(filename)
1074
+ # The rake command should include FILE= for non-default paths
1075
+ # but our tempfile is non-default, so let's verify format is correct
1076
+ expect(content).to include('rake db:seed:dump')
1077
+ end
1078
+
1079
+ it 'should include special options like INSERT_ALL and UPSERT_ALL' do
1080
+ SeedDump.dump(Sample, file: filename, header: true, insert_all: true)
1081
+ content = File.read(filename)
1082
+ expect(content).to include('INSERT_ALL=true')
1083
+ expect(content).to include('insert_all: true')
1084
+ end
1085
+ end
1086
+
1087
+ context 'created_on/updated_on columns (issue #128)' do
1088
+ # Rails supports both created_at/updated_at AND created_on/updated_on as
1089
+ # timestamp columns. Both should be excluded by default since they're
1090
+ # auto-generated by Rails and should not be manually seeded.
1091
+
1092
+ before(:each) do
1093
+ TimestampOnSample.create!(name: 'test1')
1094
+ TimestampOnSample.create!(name: 'test2')
1095
+ end
1096
+
1097
+ it 'should exclude created_on and updated_on columns by default' do
1098
+ result = SeedDump.dump(TimestampOnSample)
1099
+ expect(result).to include('TimestampOnSample.create!')
1100
+ expect(result).to include('name: "test1"')
1101
+ expect(result).to include('name: "test2"')
1102
+ # These columns should be excluded by default (like created_at/updated_at)
1103
+ expect(result).not_to include('created_on')
1104
+ expect(result).not_to include('updated_on')
1105
+ end
1106
+
1107
+ it 'should include created_on/updated_on when explicitly excluded from exclude list' do
1108
+ result = SeedDump.dump(TimestampOnSample, exclude: [:id])
1109
+ expect(result).to include('created_on')
1110
+ expect(result).to include('updated_on')
1111
+ end
1112
+ end
175
1113
  end
176
1114
  end