hover-ruby-client 0.3.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.
@@ -0,0 +1,8 @@
1
+ require './test/test_helper'
2
+
3
+ require 'hover/decoder/json_stream'
4
+
5
+ describe Hover::Decoder::JSONStream do
6
+ let(:instance) { Hover::Decoder::JSONStream.new }
7
+
8
+ end
@@ -0,0 +1,135 @@
1
+ require './test/test_helper'
2
+
3
+ require 'hover/encoder/json_stream'
4
+
5
+ describe Hover::Encoder::JSONStream do
6
+ let(:instance) { Hover::Encoder::JSONStream.new }
7
+
8
+ let(:attributes) do
9
+ {'a' => 1, 'b' => 2}
10
+ end
11
+
12
+ it "initializes" do
13
+ instance.must_be_kind_of(Hover::Encoder::JSONStream)
14
+ end
15
+
16
+ describe "open" do
17
+ it "sets file" do
18
+ file = mock
19
+ File.expects(:open).with("/tmp/filename", 'w').returns(file)
20
+
21
+ instance.open("/tmp/filename")
22
+ instance.file.must_equal(file)
23
+ end
24
+ end
25
+
26
+ describe "close" do
27
+ it "closes file" do
28
+ file = mock
29
+ file.expects(:close)
30
+
31
+ instance.file = file
32
+ instance.close
33
+
34
+ assert_nil instance.file
35
+ end
36
+ end
37
+
38
+ describe "append" do
39
+ it "appends" do
40
+ file = mock
41
+ file.expects(:puts).with("#{attributes.merge({'class' => 'Hash'}).to_json}")
42
+
43
+ instance.file = file
44
+
45
+ instance.append(attributes, attributes.class)
46
+ end
47
+
48
+ it "last" do
49
+ file = mock
50
+ file.expects(:puts).with("#{attributes.merge({'class' => 'Hash'}).to_json}")
51
+
52
+ instance.file = file
53
+
54
+ instance.append(attributes, attributes.class)
55
+ end
56
+ end
57
+
58
+ describe "append_each" do
59
+ it "appends" do
60
+ file = mock
61
+ file.expects(:puts).with("#{attributes.merge({'class' => 'Hash'}).to_json}").twice
62
+
63
+ instance.file = file
64
+
65
+ instance.append_each([attributes, attributes], attributes.class)
66
+ end
67
+ end
68
+
69
+ describe "append_records" do
70
+ it "appends from scope" do
71
+ TestClass.destroy_all
72
+
73
+ a = TestClass.create!
74
+ b = TestClass.create!
75
+
76
+ instance.expects(:append).with(a.attributes, TestClass)
77
+ instance.expects(:append).with(b.attributes, TestClass)
78
+
79
+ instance.append_records(TestClass.all)
80
+ end
81
+ end
82
+
83
+ describe "valid json" do
84
+
85
+ it "avoids NaN problem" do
86
+ # with PostgreSQL we could use TestClass#a, but sqlite persists the NaN as nil
87
+ class SpecialStandInForThisTest
88
+ def find_each
89
+ yield self
90
+ end
91
+ def attributes
92
+ # if JSONStream doesn't turn this NaN into a nil we'll see an error in parsing the generated JSON
93
+ nan = 0.0 / 0.0
94
+ {"foo" => "bar", "x" => nan}
95
+ end
96
+ end
97
+ file = Tempfile.new(['hover-ruby-client_json_encoder-NaN', '.json'])
98
+ instance.open(file.path)
99
+ instance.append_records(SpecialStandInForThisTest.new)
100
+ instance.close
101
+ JSON.parse(file.read).must_equal({"foo" => "bar", "x" => nil, "class"=>"SpecialStandInForThisTest"})
102
+ end
103
+
104
+ it "works" do
105
+ file = Tempfile.new(['hover-ruby-client_json_encoder', '.json'])
106
+
107
+ begin
108
+ 1.upto(50) do
109
+ TestClass.create!
110
+ end
111
+
112
+ 1.upto(50) do
113
+ Tag.create!
114
+ end
115
+
116
+ instance.open(file.path)
117
+ instance.append_records(TestClass.all)
118
+ instance.append_records(Tag.all)
119
+ instance.close
120
+
121
+ expected = [
122
+ TestClass.all.map { |test_class| test_class.attributes.merge({'class' => 'TestClass'}) },
123
+ Tag.all.map { |tag| tag.attributes.merge({'class' => 'Tag'}) },
124
+ ].flatten
125
+
126
+ file.each_line do |line|
127
+ expected.include?(JSON.parse(line)).must_equal(true)
128
+ end
129
+ ensure
130
+ file.close
131
+ file.unlink
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,354 @@
1
+ require './test/test_helper'
2
+
3
+ require 'hover/importer/active_record'
4
+ require 'active_record'
5
+
6
+ class TestClass < ActiveRecord::Base
7
+ belongs_to :item
8
+
9
+ def self.s3_file_versions
10
+ {image: {small: {}}, document: {}}
11
+ end
12
+
13
+ def document_s3_keys=(value)
14
+ value
15
+ end
16
+
17
+ def image_s3_keys=(value)
18
+ value
19
+ end
20
+ end
21
+
22
+ class ScopedTestClass < ActiveRecord::Base
23
+ belongs_to :item
24
+
25
+ default_scope -> { where(a: 1) }
26
+ end
27
+
28
+ class Item < ActiveRecord::Base
29
+ has_many :tags, as: :taggable, dependent: :destroy
30
+ end
31
+
32
+ class Tag < ActiveRecord::Base
33
+ belongs_to :taggable, polymorphic: true
34
+ end
35
+
36
+ describe Hover::Importer::ActiveRecord do
37
+ let(:record) { TestClass.new }
38
+ let(:update_finder) { ->(importer, klass, attributes) { record } }
39
+ let(:remote_local_map) { {table_name: {'remote_id' => 'local_id'}} }
40
+ let(:importer) { Hover::Importer::ActiveRecord.new(remote_local_map, update_finder) }
41
+
42
+ let(:results) do
43
+ {
44
+ "state" => "complete",
45
+ "results" => {
46
+ "test_classes" => [{"id" => 1}, {"id" => 2}]
47
+ }
48
+ }
49
+ end
50
+
51
+ describe "initialize" do
52
+ it "sets initial values" do
53
+ importer.remote_local_map.must_equal(remote_local_map)
54
+ assert_equal importer.update_finder, update_finder
55
+ importer.column_changes.must_equal({})
56
+ end
57
+ end
58
+
59
+ describe "update" do
60
+ it "calls update_record for each" do
61
+ importer.expects(:update_record).with(TestClass, {"id" => 1}).once
62
+ importer.expects(:update_record).with(TestClass, {"id" => 2}).once
63
+
64
+ importer.update("test_classes", results["results"].delete("test_classes"))
65
+ end
66
+
67
+ it "returns updated records" do
68
+ importer.expects(:update_record).twice.returns([TestClass, 1])
69
+ importer.update("test_classes", results["results"].delete("test_classes")).must_equal([[TestClass, 1], [TestClass, 1]])
70
+ end
71
+ end
72
+
73
+ describe "update_record" do
74
+ it "does nothing if the record to update is not found" do
75
+ importer.expects(:find_record_to_update).returns(nil)
76
+ assert_nil importer.update_record(TestClass, mock)
77
+ end
78
+
79
+ describe "saves" do
80
+ before do
81
+ end
82
+
83
+ it "does not update primary key and doesnt overwrite s3 keys" do
84
+ record.expects(:assign_attributes).with({"image_s3_keys" => nil, "document_s3_keys" => nil}).never
85
+ record.expects(:assign_attributes).with({}).once
86
+ importer.update_record(TestClass, {"id" => 1})
87
+ end
88
+
89
+ it "handles s3 file attachments" do
90
+ image_s3_key = {"small" => 100}
91
+ record.expects(:assign_attributes).with({"image_s3_keys" => image_s3_key}).once
92
+ importer.update_record(TestClass, {"id" => 1, "image" => image_s3_key})
93
+ end
94
+
95
+ it "excludes invalid column names" do
96
+ record.expects(:assign_attributes).with({}).once
97
+ record.expects(:assign_attributes).with({"image_s3_keys" => nil, "document_s3_keys" => nil}).never
98
+ importer.update_record(TestClass, {"id" => 1, "c" => 2})
99
+ end
100
+
101
+ it "includes valid column names" do
102
+ record.expects(:assign_attributes).with({"image_s3_keys" => nil, "document_s3_keys" => nil, "a" => 100}).never
103
+ record.expects(:assign_attributes).with({"a" => 100}).once
104
+ importer.update_record(TestClass, {"id" => 1, "a" => 100})
105
+ end
106
+
107
+ it "returns record" do
108
+ record.expects(:assign_attributes).with({"image_s3_keys" => nil, "document_s3_keys" => nil}).never
109
+ record.expects(:assign_attributes).with({}).once
110
+ importer.update_record(TestClass, {"id" => 1}).must_equal([TestClass, record.id])
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "s3_file_keys" do
116
+ it "returns nil if klass isn't using s3 file" do
117
+ assert_nil importer.s3_file_keys(Integer, {})
118
+ end
119
+
120
+ it "skips other attributes" do
121
+ attributes = {"foo" => 1, 'document' => 2}
122
+ importer.s3_file_keys(TestClass, attributes).must_equal({'document_s3_keys' => 2})
123
+ end
124
+
125
+ it "does not default to nil" do
126
+ importer.s3_file_keys(TestClass, {}).must_equal({})
127
+ end
128
+
129
+ it "renames s3 attributes" do
130
+ attributes = {"image" => 1}
131
+ importer.s3_file_keys(TestClass, attributes).must_equal({"image_s3_keys" => 1})
132
+ end
133
+
134
+ it "assigns incoming keys correctly" do
135
+
136
+ attributes = {
137
+ "id" => 1,
138
+ "image" => {
139
+ "image" => "Midas/Image/236/image.jpg",
140
+ "image_small" => "Midas/Image/236/image_small.jpg",
141
+ "image_medium" => "Midas/Image/236/image_medium.jpg",
142
+ "image_low_quality" => "Midas/Image/236/image_low_quality.jpg",
143
+ "image_thumb" => "Manowar/Image/172/image_thumb.jpg"
144
+ }
145
+ }
146
+
147
+ importer.expects(:insert_record).once.with(record).returns(2)
148
+ TestClass.expects(:new).with({"image_s3_keys" => attributes["image"]}).returns(record)
149
+
150
+ importer.import_record(TestClass, attributes).must_equal([TestClass, 2])
151
+
152
+ importer.remote_local_map["test_classes"][1].must_equal(2)
153
+ end
154
+ end
155
+
156
+ describe "find_record_to_update" do
157
+ it "calls update_finder" do
158
+ update_finder.expects(:call).with(importer, TestClass, {a: 1})
159
+ importer.find_record_to_update(TestClass, {a: 1})
160
+ end
161
+ end
162
+
163
+ describe "import_results" do
164
+ it "imports" do
165
+ importer.expects(:import_table).with("test_classes", [{"id" => 1}, {"id" => 2}]).once
166
+ importer.import_results(results["results"])
167
+ end
168
+
169
+ it "returns [class, id] sets for imported records" do
170
+ importer.expects(:import_record).returns([record.class, record.id]).twice
171
+ importer.import_results(results["results"]).must_equal([[TestClass, record.id], [TestClass, record.id]])
172
+ end
173
+ end
174
+
175
+ describe "import_table" do
176
+ it "imports" do
177
+ importer.expects(:import_record).with(TestClass, {"id" => 1}).once
178
+ importer.import_table("test_classes", [{"id" => 1}])
179
+ end
180
+
181
+ it "returns [class, id] sets for imported records" do
182
+ importer.expects(:import_record).returns([record.class, record.id]).twice
183
+ importer.import_table("test_classes", [{}, {}]).must_equal([[TestClass, record.id], [TestClass, record.id]])
184
+ end
185
+ end
186
+
187
+ describe "import_record" do
188
+ it "returns nil if insert fails" do
189
+ importer.expects(:insert_record).once.returns(nil)
190
+ TestClass.expects(:find).never
191
+ assert_nil importer.import_record(TestClass, {})
192
+ end
193
+
194
+ it "imports" do
195
+ importer.expects(:insert_record).once.with(record).returns(2)
196
+ TestClass.expects(:new).with({}).returns(record)
197
+
198
+ importer.import_record(TestClass, {"id" => 1}).must_equal([TestClass, 2])
199
+
200
+ importer.remote_local_map["test_classes"][1].must_equal(2)
201
+ end
202
+
203
+ it "filters out attributes that don't have corresponding columns in the local database" do
204
+ class TestClass
205
+ def attributes
206
+ super.merge('upstream_attribute' => 1)
207
+ end
208
+ end
209
+
210
+ starting_count = TestClass.count
211
+ importer.import_record(TestClass, {})
212
+ assert_equal (starting_count + 1), TestClass.count
213
+ end
214
+ end
215
+
216
+ describe "insert_record" do
217
+ it "inserts" do
218
+ TestClass.expects(:_insert_record).returns(1)
219
+
220
+ attributes = {"id" => 33}
221
+ record = TestClass.new(attributes)
222
+
223
+ importer.insert_record(record).must_equal(1)
224
+ end
225
+
226
+
227
+ end
228
+
229
+ describe "new_foreign_keys" do
230
+ it do
231
+ record.item_id = 2
232
+ importer.remote_local_map["items"] = {2 => 1}
233
+
234
+ importer.new_foreign_keys(record).must_equal({"item_id" => 1})
235
+ end
236
+
237
+ it "processes belongs_to macro" do
238
+ record.item_id = 2
239
+ association = record.class.reflect_on_all_associations.first
240
+ importer.expects(:new_foreign_keys_for_belongs_to).with(record, association)
241
+ importer.new_foreign_keys(record)
242
+ end
243
+
244
+ it "processes polymorphic belongs_to macro" do
245
+ tag = Tag.create!(name: 'tag', taggable_type: Item.name, taggable_id: 1)
246
+ association = tag.class.reflect_on_all_associations.first
247
+ importer.expects(:new_foreign_keys_for_belongs_to_polymorphic).with(tag, association)
248
+ importer.new_foreign_keys(tag)
249
+ end
250
+
251
+ it "supports polymorphic relationships" do
252
+ tag = Tag.create!(name: 'tag', taggable_type: Item.name, taggable_id: 1)
253
+ importer.remote_local_map["items"] = {1 => 3}
254
+ importer.new_foreign_keys(tag).must_equal({"taggable_id" => 3})
255
+ end
256
+ end
257
+
258
+ describe "new_foreign_keys_belongs_to" do
259
+ it do
260
+ record.item_id = 2
261
+ importer.remote_local_map["items"] = {2 => 1}
262
+ association = record.class.reflect_on_all_associations.first
263
+
264
+ importer.new_foreign_keys_for_belongs_to(record, association).must_equal(['item_id', 1])
265
+ end
266
+ end
267
+
268
+ describe "new_foreign_keys_belongs_to_polymorphic" do
269
+ it do
270
+ tag = Tag.create!(name: 'tag', taggable_type: Item.name, taggable_id: 1)
271
+ association = tag.class.reflect_on_all_associations.first
272
+ importer.remote_local_map["items"] = {1 => 3}
273
+ importer.new_foreign_keys_for_belongs_to_polymorphic(tag, association).must_equal(["taggable_id", 3])
274
+ end
275
+ end
276
+
277
+ describe "update_foreign_keys" do
278
+ it "does nothing with no new foreign keys" do
279
+ assert_nil importer.update_foreign_keys(Item.new)
280
+ end
281
+
282
+ it "updates foreign keys" do
283
+ record.item_id = 2
284
+ importer.remote_local_map["items"] = {2 => 1}
285
+
286
+ TestClass.const_get(:ActiveRecord_Relation).any_instance.expects(:update_all).once.with({'item_id' => 1})
287
+ record.expects(:reload).returns(record)
288
+
289
+ importer.update_foreign_keys(record).must_equal([TestClass, record.id])
290
+ end
291
+
292
+ it "updates foreign keys db" do
293
+ record.item_id = 2
294
+ record.save!
295
+
296
+ importer.remote_local_map["items"] = {2 => 1}
297
+
298
+ importer.update_foreign_keys(record).must_equal([TestClass, record.id])
299
+
300
+ record.reload
301
+ assert_equal 1, record.item_id
302
+ end
303
+
304
+ it "skips default scopes" do
305
+ item_id = 2
306
+ a = 4
307
+ scoped_record = ScopedTestClass.create!(a: a, item_id: item_id)
308
+
309
+ importer.remote_local_map["items"] = {2 => 1}
310
+
311
+ importer.update_foreign_keys(scoped_record)
312
+
313
+ scoped_record.reload
314
+ assert_equal 1, scoped_record.item_id
315
+ end
316
+
317
+ it "updates polymorphic foreign keys" do
318
+ tag = Tag.create!(name: 'tag', taggable_type: Item.name, taggable_id: 2)
319
+ importer.remote_local_map["items"] = {2 => 1}
320
+
321
+ Tag.const_get(:ActiveRecord_Relation).any_instance.expects(:update_all).once.with({'taggable_id' => 1})
322
+ tag.expects(:reload).returns(tag)
323
+
324
+ importer.update_foreign_keys(tag).must_equal([Tag, tag.id])
325
+ end
326
+ end
327
+
328
+ describe "column_changes" do
329
+ describe "change_columns" do
330
+ it "changes" do
331
+ importer.column_changes = {"building_version_id" => "model_id"}
332
+ importer.change_columns({"building_version_id" => 2}).must_equal({"model_id" => 2})
333
+ end
334
+
335
+ it "doesn't affect other columns" do
336
+ importer.column_changes = {"a" => "b"}
337
+ importer.change_columns({"c" => 2}).must_equal({"c" => 2})
338
+ end
339
+ end
340
+
341
+ it "change during update" do
342
+ importer.column_changes["building_version_id"] = "item_id"
343
+ record.expects(:save!)
344
+ importer.update(TestClass.table_name, [{"building_version_id" => 2, "id" => 1}]).must_equal([[record.class, record.id]])
345
+ record.item_id.must_equal(2)
346
+ end
347
+
348
+ it "change during insert" do
349
+ importer.column_changes["building_version_id"] = "item_id"
350
+ importer.expects(:import_record).with(TestClass, {"item_id" => 2, "id" => 1}).once.returns([TestClass, record.id])
351
+ importer.import_table(TestClass.table_name, [{"item_id" => 2, "id" => 1}]).must_equal([[TestClass, record.id]])
352
+ end
353
+ end
354
+ end