netsuite_rails 0.1.0 → 0.2.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.
- data/.gitignore +3 -0
- data/.travis.yml +3 -0
- data/Gemfile +17 -7
- data/README.md +9 -4
- data/lib/netsuite_rails/configuration.rb +3 -1
- data/lib/netsuite_rails/list_sync/poll_manager.rb +30 -0
- data/lib/netsuite_rails/list_sync.rb +2 -2
- data/lib/netsuite_rails/netsuite_rails.rb +13 -3
- data/lib/netsuite_rails/poll_timestamp.rb +5 -0
- data/lib/netsuite_rails/{poll_manager.rb → poll_trigger.rb} +18 -3
- data/lib/netsuite_rails/record_sync/poll_manager.rb +186 -0
- data/lib/netsuite_rails/record_sync/pull_manager.rb +10 -137
- data/lib/netsuite_rails/record_sync/push_manager.rb +59 -21
- data/lib/netsuite_rails/record_sync.rb +142 -140
- data/lib/netsuite_rails/spec/spec_helper.rb +34 -6
- data/lib/netsuite_rails/sync_trigger.rb +80 -75
- data/lib/netsuite_rails/tasks/netsuite.rb +26 -30
- data/lib/netsuite_rails/url_helper.rb +12 -3
- data/netsuite_rails.gemspec +3 -3
- data/spec/models/poll_manager_spec.rb +45 -0
- data/spec/models/poll_trigger_spec.rb +26 -0
- data/spec/models/record_sync/push_manager_spec.rb +0 -0
- data/spec/models/spec_helper_spec.rb +29 -0
- data/spec/models/sync_trigger_spec.rb +62 -0
- data/spec/models/url_helper_spec.rb +23 -0
- data/spec/spec_helper.rb +17 -1
- data/spec/support/config/database.yml +11 -0
- data/spec/support/dynamic_models/class_builder.rb +48 -0
- data/spec/support/dynamic_models/model_builder.rb +83 -0
- data/spec/support/example_models.rb +31 -0
- data/spec/support/netsuite_rails.rb +1 -0
- data/spec/support/test_application.rb +55 -0
- metadata +36 -10
- data/lib/netsuite_rails/list_sync/pull_manager.rb +0 -33
@@ -1,104 +1,109 @@
|
|
1
1
|
module NetSuiteRails
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
2
|
+
module SyncTrigger
|
3
|
+
extend self
|
4
|
+
|
5
|
+
# TODO think about a flag to push to NS on after_validation vs after_commit
|
6
|
+
# TODO think about background async record syncing (re: multiple sales order updates)
|
7
|
+
# TODO need to add hook for custom proc to determine if data should be pushed to netsuite
|
8
|
+
# if a model has a pending/complete state we might want to only push on complete
|
9
|
+
|
10
|
+
def attach(klass)
|
11
|
+
if klass.include?(SubListSync)
|
12
|
+
klass.after_save { SyncTrigger.sublist_trigger(self) }
|
13
|
+
klass.after_destroy { SyncTrigger.sublist_trigger(self) }
|
14
|
+
elsif klass.include?(RecordSync)
|
15
|
+
|
16
|
+
# during the initial pull we don't want to push changes up
|
17
|
+
klass.before_save do
|
18
|
+
@netsuite_sync_record_import = self.new_record? && self.netsuite_id.present?
|
19
|
+
|
20
|
+
# if false record will not save
|
21
|
+
true
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
klass.after_save do
|
25
|
+
# need to implement this conditional on the save hook level
|
26
|
+
# because the coordination class doesn't know about model persistence state
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
if @netsuite_sync_record_import
|
29
|
+
# pull the record down if it has't been pulled yet
|
30
|
+
# this is useful when this is triggered by a save on a parent record which has this
|
31
|
+
# record as a related record
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
end
|
36
|
-
else
|
37
|
-
SyncTrigger.record_push_trigger(self)
|
33
|
+
unless self.netsuite_pulled?
|
34
|
+
SyncTrigger.record_pull_trigger(self)
|
38
35
|
end
|
39
|
-
|
40
|
-
|
36
|
+
else
|
37
|
+
SyncTrigger.record_push_trigger(self)
|
41
38
|
end
|
42
|
-
end
|
43
39
|
|
44
|
-
|
40
|
+
@netsuite_sync_record_import = false
|
41
|
+
end
|
45
42
|
end
|
46
43
|
|
47
|
-
|
48
|
-
|
44
|
+
# TODO think on NetSuiteRails::ListSync
|
45
|
+
end
|
49
46
|
|
50
|
-
|
51
|
-
|
47
|
+
def record_pull_trigger(local)
|
48
|
+
return if NetSuiteRails::Configuration.netsuite_pull_disabled
|
49
|
+
|
50
|
+
record_trigger_action(local, :netsuite_pull)
|
51
|
+
end
|
52
|
+
|
53
|
+
def record_push_trigger(netsuite_record_rep)
|
54
|
+
# don't update when fields are updated because of a netsuite_pull
|
55
|
+
return if netsuite_record_rep.netsuite_pulling?
|
52
56
|
|
53
|
-
|
54
|
-
# don't update when fields are updated because of a netsuite_pull
|
55
|
-
return if netsuite_record_rep.netsuite_pulling?
|
57
|
+
return if NetSuiteRails::Configuration.netsuite_push_disabled
|
56
58
|
|
57
|
-
|
59
|
+
# don't update if a read only record
|
60
|
+
return if netsuite_record_rep.netsuite_sync == :read
|
58
61
|
|
59
|
-
|
60
|
-
return if netsuite_record_rep.netsuite_sync == :read
|
62
|
+
sync_options = netsuite_record_rep.netsuite_sync_options
|
61
63
|
|
62
|
-
|
64
|
+
# :if option is a block that returns a boolean
|
65
|
+
return if sync_options.has_key?(:if) && !netsuite_record_rep.instance_exec(&sync_options[:if])
|
63
66
|
|
64
|
-
|
65
|
-
|
67
|
+
record_trigger_action(netsuite_record_rep, :netsuite_push)
|
68
|
+
end
|
69
|
+
|
70
|
+
def record_trigger_action(local, action)
|
71
|
+
sync_options = local.netsuite_sync_options
|
72
|
+
|
73
|
+
action_options = {
|
66
74
|
|
67
|
-
|
75
|
+
}
|
76
|
+
|
77
|
+
if sync_options.has_key?(:credentials)
|
78
|
+
action_options[:credentials] = local.instance_exec(&sync_options[:credentials])
|
68
79
|
end
|
69
80
|
|
70
|
-
|
71
|
-
|
81
|
+
# TODO need to pass off the credentials to the NS push command
|
82
|
+
|
83
|
+
# You can force sync mode in different envoirnments with the global configuration variables
|
72
84
|
|
73
|
-
|
74
|
-
|
75
|
-
|
85
|
+
if sync_options[:mode] == :sync || NetSuiteRails::Configuration.netsuite_sync_mode == :sync
|
86
|
+
local.send(action)
|
87
|
+
else
|
88
|
+
action_options[:modified_fields] = NetSuiteRails::RecordSync::PushManager.modified_local_fields(local).keys
|
76
89
|
|
77
|
-
# TODO
|
78
|
-
|
79
|
-
# You can force sync mode in different envoirnments with the global configuration variables
|
90
|
+
# TODO support the rails4 DJ implementation
|
80
91
|
|
81
|
-
if
|
82
|
-
local.send(action)
|
92
|
+
if local.respond_to?(:delay)
|
93
|
+
local.delay.send(action, action_options)
|
83
94
|
else
|
84
|
-
|
85
|
-
|
86
|
-
if local.respond_to?(:delay)
|
87
|
-
local.delay.send(action)
|
88
|
-
else
|
89
|
-
raise 'no supported delayed job method found'
|
90
|
-
end
|
95
|
+
raise 'no supported delayed job method found'
|
91
96
|
end
|
92
97
|
end
|
98
|
+
end
|
93
99
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
100
|
+
def sublist_trigger(sublist_item_rep)
|
101
|
+
parent = sublist_item_rep.send(sublist_item_rep.class.netsuite_sublist_parent)
|
102
|
+
|
103
|
+
if parent.class.include?(RecordSync)
|
104
|
+
record_push_trigger(parent)
|
100
105
|
end
|
101
|
-
|
102
106
|
end
|
107
|
+
|
103
108
|
end
|
104
109
|
end
|
@@ -1,21 +1,8 @@
|
|
1
1
|
namespace :netsuite do
|
2
2
|
|
3
|
-
|
4
|
-
task :fresh_sync => :environment do
|
5
|
-
NetSuiteRails::PollTimestamp.delete_all
|
6
|
-
|
7
|
-
ENV['SKIP_EXISTING'] = "true"
|
8
|
-
|
9
|
-
Rake::Task["netsuite:sync"].invoke
|
10
|
-
end
|
11
|
-
|
12
|
-
desc "sync all netsuite records"
|
13
|
-
task :sync => :environment do
|
14
|
-
# need to eager load to ensure that all classes are loaded into the poll manager
|
15
|
-
Rails.application.eager_load!
|
16
|
-
|
3
|
+
def generate_options
|
17
4
|
opts = {
|
18
|
-
skip_existing: ENV['SKIP_EXISTING'].present?
|
5
|
+
skip_existing: ENV['SKIP_EXISTING'].present? && ENV['SKIP_EXISTING'] == "true"
|
19
6
|
}
|
20
7
|
|
21
8
|
if ENV['RECORD_MODELS'].present?
|
@@ -26,27 +13,36 @@ namespace :netsuite do
|
|
26
13
|
opts[:list_models] = ENV['LIST_MODELS'].split(',').map(&:constantize)
|
27
14
|
end
|
28
15
|
|
29
|
-
# TODO make push disabled configurable
|
30
|
-
|
31
16
|
# field values might change on import because of remote data structure changes
|
32
17
|
# stop all pushes on sync & fresh_sync to avoid pushing up data that really hasn't
|
33
18
|
# changed for each record
|
34
19
|
|
20
|
+
# TODO make push disabled configurable
|
35
21
|
NetSuiteRails::Configuration.netsuite_push_disabled true
|
36
22
|
|
37
|
-
|
23
|
+
opts
|
38
24
|
end
|
39
25
|
|
40
|
-
|
26
|
+
desc "Sync all NetSuite records using import_all"
|
27
|
+
task :fresh_sync => :environment do
|
28
|
+
NetSuiteRails::PollTimestamp.delete_all
|
29
|
+
|
30
|
+
ENV['SKIP_EXISTING'] = "true"
|
31
|
+
|
32
|
+
Rake::Task["netsuite:sync"].invoke
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "sync all netsuite records"
|
36
|
+
task :sync => :environment do
|
37
|
+
# need to eager load to ensure that all classes are loaded into the poll manager
|
38
|
+
Rails.application.eager_load!
|
41
39
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
# end
|
52
|
-
# end
|
40
|
+
NetSuiteRails::PollTrigger.sync(self.generate_options)
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "sync all local netsuite records"
|
44
|
+
task :sync_local => :environment do
|
45
|
+
NetSuiteRails::PollTrigger.update_local_records(generate_options)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -6,13 +6,22 @@ module NetSuiteRails
|
|
6
6
|
def self.netsuite_url(record = self)
|
7
7
|
prefix = "https://system#{".sandbox" if NetSuite::Configuration.sandbox}.netsuite.com/app"
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
if record.class.to_s.start_with?('NetSuite::Records')
|
10
|
+
record_class = record.class
|
11
|
+
internal_id = record.internal_id
|
12
|
+
is_custom_record = false
|
13
|
+
else
|
14
|
+
record_class = record.netsuite_record_class
|
15
|
+
internal_id = record.netsuite_id
|
16
|
+
is_custom_record = record.netsuite_custom_record?
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO support NS classes, should jump right to the list for the class
|
11
20
|
|
12
21
|
# https://system.sandbox.netsuite.com/app/common/scripting/scriptrecordlist.nl
|
13
22
|
# https://system.sandbox.netsuite.com/app/common/scripting/script.nl
|
14
23
|
|
15
|
-
if
|
24
|
+
if is_custom_record
|
16
25
|
"#{prefix}/common/custom/custrecordentry.nl?id=#{internal_id}&rectype=#{record.class.netsuite_custom_record_type_id}"
|
17
26
|
elsif [ NetSuite::Records::InventoryItem, NetSuite::Records::NonInventorySaleItem, NetSuite::Records::AssemblyItem].include?(record_class)
|
18
27
|
"#{prefix}/common/item/item.nl?id=#{internal_id}"
|
data/netsuite_rails.gemspec
CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "netsuite_rails"
|
7
|
-
s.version = "0.
|
7
|
+
s.version = "0.2.0"
|
8
8
|
s.authors = ["Michael Bianco"]
|
9
9
|
s.email = ["mike@cliffsidemedia.com"]
|
10
10
|
s.summary = %q{Write Rails applications that integrate with NetSuite}
|
@@ -16,10 +16,10 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
17
|
s.require_paths = ["lib"]
|
18
18
|
|
19
|
-
s.add_dependency 'netsuite', '~> 0.3'
|
19
|
+
s.add_dependency 'netsuite', '~> 0.3.2'
|
20
20
|
s.add_dependency 'rails', '>= 3.2.16'
|
21
21
|
|
22
22
|
s.add_development_dependency "bundler", "~> 1.6"
|
23
23
|
s.add_development_dependency "rake"
|
24
|
-
s.add_development_dependency "rspec"
|
24
|
+
s.add_development_dependency "rspec", '~> 3.1'
|
25
25
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NetSuiteRails::RecordSync::PollManager do
|
4
|
+
include ExampleModels
|
5
|
+
|
6
|
+
# TODO fake a couple items in the list
|
7
|
+
|
8
|
+
let(:empty_search_results) { OpenStruct.new(results: [ OpenStruct.new(internal_id: 0) ]) }
|
9
|
+
|
10
|
+
it "should poll record sync objects" do
|
11
|
+
allow(NetSuite::Records::Customer).to receive(:search).and_return(empty_search_results)
|
12
|
+
|
13
|
+
StandardRecord.netsuite_poll(import_all: true)
|
14
|
+
|
15
|
+
expect(NetSuite::Records::Customer).to have_received(:search)
|
16
|
+
end
|
17
|
+
|
18
|
+
skip "should poll and then get_list on saved search" do
|
19
|
+
# TODO SS enabled record
|
20
|
+
# TODO mock search to return one result
|
21
|
+
# TODO mock out get_list
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should poll list sync objects" do
|
25
|
+
allow(NetSuite::Records::CustomList).to receive(:get).and_return(OpenStruct.new(custom_value_list: OpenStruct.new(custom_value: [])))
|
26
|
+
|
27
|
+
StandardList.netsuite_poll(import_all: true)
|
28
|
+
|
29
|
+
expect(NetSuite::Records::CustomList).to have_received(:get)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should sync only available local records" do
|
33
|
+
NetSuiteRails::Configuration.netsuite_push_disabled true
|
34
|
+
StandardRecord.create! netsuite_id: 123
|
35
|
+
NetSuiteRails::Configuration.netsuite_push_disabled false
|
36
|
+
|
37
|
+
allow(NetSuite::Records::Customer).to receive(:get_list).and_return([OpenStruct.new(internal_id: 123)])
|
38
|
+
allow(NetSuiteRails::RecordSync::PollManager).to receive(:process_search_result_item)
|
39
|
+
|
40
|
+
NetSuiteRails::PollTrigger.update_local_records
|
41
|
+
|
42
|
+
expect(NetSuite::Records::Customer).to have_received(:get_list)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NetSuiteRails::PollTrigger do
|
4
|
+
include ExampleModels
|
5
|
+
|
6
|
+
it "should properly sync for the first time" do
|
7
|
+
allow(StandardRecord).to receive(:netsuite_poll).with(hash_including(:import_all => true))
|
8
|
+
|
9
|
+
NetSuiteRails::PollTrigger.sync list_models: []
|
10
|
+
|
11
|
+
expect(StandardRecord).to have_received(:netsuite_poll)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should trigger syncing when the time has passed is greater than frequency" do
|
15
|
+
allow(StandardRecord).to receive(:netsuite_poll)
|
16
|
+
|
17
|
+
StandardRecord.netsuite_sync_options[:frequency] = 5.minutes
|
18
|
+
timestamp = NetSuiteRails::PollTimestamp.for_class(StandardRecord)
|
19
|
+
timestamp.value = DateTime.now - 7.minutes
|
20
|
+
timestamp.save!
|
21
|
+
|
22
|
+
NetSuiteRails::PollTrigger.sync list_models: []
|
23
|
+
|
24
|
+
expect(StandardRecord).to have_received(:netsuite_poll)
|
25
|
+
end
|
26
|
+
end
|
File without changes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'netsuite_rails/spec/spec_helper'
|
4
|
+
|
5
|
+
describe NetSuiteRails::TestHelpers do
|
6
|
+
include NetSuiteRails::TestHelpers
|
7
|
+
include ExampleModels
|
8
|
+
|
9
|
+
let(:fake_search_results) { OpenStruct.new(results: [ OpenStruct.new(internal_id: 0) ]) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(NetSuite::Records::Customer).to receive(:search).and_return(fake_search_results)
|
13
|
+
allow(NetSuite::Records::Customer).to receive(:get)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should accept a standard NS gem object" do
|
17
|
+
get_last_netsuite_object(NetSuite::Records::Customer)
|
18
|
+
|
19
|
+
expect(NetSuite::Records::Customer).to have_received(:search)
|
20
|
+
expect(NetSuite::Records::Customer).to have_received(:get)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should accept a record sync enabled object" do
|
24
|
+
get_last_netsuite_object(StandardRecord.new)
|
25
|
+
|
26
|
+
expect(NetSuite::Records::Customer).to have_received(:search)
|
27
|
+
expect(NetSuite::Records::Customer).to have_received(:get)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NetSuiteRails::SyncTrigger do
|
4
|
+
include ExampleModels
|
5
|
+
|
6
|
+
before do
|
7
|
+
allow(NetSuiteRails::RecordSync::PushManager).to receive(:push_add)
|
8
|
+
allow(NetSuiteRails::RecordSync::PushManager).to receive(:push_update)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should push new record when saved" do
|
12
|
+
s = StandardRecord.new
|
13
|
+
s.phone = Faker::PhoneNumber.phone_number
|
14
|
+
s.save!
|
15
|
+
|
16
|
+
expect(NetSuiteRails::RecordSync::PushManager).to have_received(:push_add)
|
17
|
+
expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_update)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should not push update on a pull record" do
|
21
|
+
s = StandardRecord.new netsuite_id: 123
|
22
|
+
allow(s).to receive(:netsuite_pull)
|
23
|
+
s.save!
|
24
|
+
|
25
|
+
expect(s).to have_received(:netsuite_pull)
|
26
|
+
expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_add)
|
27
|
+
expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_update)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should push an update on an existing record" do
|
31
|
+
s = StandardRecord.new netsuite_id: 123
|
32
|
+
allow(s).to receive(:netsuite_pull)
|
33
|
+
s.save!
|
34
|
+
|
35
|
+
s.phone = Faker::PhoneNumber.phone_number
|
36
|
+
s.save!
|
37
|
+
|
38
|
+
expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_add)
|
39
|
+
expect(NetSuiteRails::RecordSync::PushManager).to have_received(:push_update)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should push the modified attributes to the model" do
|
43
|
+
s = StandardRecord.new netsuite_id: 123
|
44
|
+
allow(s).to receive(:netsuite_pull)
|
45
|
+
s.save!
|
46
|
+
|
47
|
+
# delayed_job isn't included in this gem; hack it into the current record instance
|
48
|
+
s.instance_eval { def delay; self; end }
|
49
|
+
allow(s).to receive(:delay).and_return(s)
|
50
|
+
|
51
|
+
NetSuiteRails::Configuration.netsuite_sync_mode :async
|
52
|
+
|
53
|
+
s.phone = Faker::PhoneNumber.phone_number
|
54
|
+
s.save!
|
55
|
+
|
56
|
+
NetSuiteRails::Configuration.netsuite_sync_mode :sync
|
57
|
+
|
58
|
+
expect(s).to have_received(:delay)
|
59
|
+
expect(NetSuiteRails::RecordSync::PushManager).to have_received(:push_update).with(anything, anything, {:modified_fields=>{:phone=> :phone}})
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe NetSuiteRails::UrlHelper do
|
4
|
+
include ExampleModels
|
5
|
+
|
6
|
+
it 'should handle a netsuite record' do
|
7
|
+
c = NetSuite::Records::Customer.new internal_id: 123
|
8
|
+
url = NetSuiteRails::UrlHelper.netsuite_url(c)
|
9
|
+
|
10
|
+
expect(url).to include("cust")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should handle a record sync enabled record" do
|
14
|
+
s = StandardRecord.new netsuite_id: 123
|
15
|
+
url = NetSuiteRails::UrlHelper.netsuite_url(s)
|
16
|
+
|
17
|
+
expect(url).to include("cust")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should handle a list sync enabled record" do
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,20 @@
|
|
1
|
+
require 'rails/all'
|
2
|
+
|
3
|
+
require 'shoulda/matchers'
|
4
|
+
require 'rspec/rails'
|
5
|
+
require 'faker'
|
6
|
+
require 'pry'
|
7
|
+
|
8
|
+
require 'netsuite_rails'
|
9
|
+
|
10
|
+
Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f }
|
11
|
+
|
12
|
+
TestApplication::Application.initialize!
|
13
|
+
NetSuiteRails::PollTimestamp.delete_all
|
14
|
+
|
1
15
|
RSpec.configure do |config|
|
16
|
+
config.color = true
|
17
|
+
|
2
18
|
config.expect_with :rspec do |expectations|
|
3
19
|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
4
20
|
end
|
@@ -6,4 +22,4 @@ RSpec.configure do |config|
|
|
6
22
|
config.mock_with :rspec do |mocks|
|
7
23
|
mocks.verify_partial_doubles = true
|
8
24
|
end
|
9
|
-
end
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# https://github.com/thoughtbot/shoulda-matchers/raw/2a4b9f1e163fa9c5b84f223962d5f8e099032420/spec/support/class_builder.rb
|
2
|
+
|
3
|
+
module ClassBuilder
|
4
|
+
def self.included(example_group)
|
5
|
+
example_group.class_eval do
|
6
|
+
after do
|
7
|
+
teardown_defined_constants
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def define_class(class_name, base = Object, &block)
|
13
|
+
class_name = class_name.to_s.camelize
|
14
|
+
|
15
|
+
if Object.const_defined?(class_name)
|
16
|
+
Object.__send__(:remove_const, class_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
# FIXME: ActionMailer 3.2 calls `name.underscore` immediately upon
|
20
|
+
# subclassing. Class.new.name == nil. So, Class.new(ActionMailer::Base)
|
21
|
+
# errors out since it's trying to do `nil.underscore`. This is very ugly but
|
22
|
+
# allows us to test against ActionMailer 3.2.x.
|
23
|
+
eval <<-A_REAL_CLASS_FOR_ACTION_MAILER_3_2
|
24
|
+
class ::#{class_name} < #{base}
|
25
|
+
end
|
26
|
+
A_REAL_CLASS_FOR_ACTION_MAILER_3_2
|
27
|
+
|
28
|
+
Object.const_get(class_name).tap do |constant_class|
|
29
|
+
constant_class.unloadable
|
30
|
+
|
31
|
+
if block_given?
|
32
|
+
constant_class.class_eval(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
if constant_class.respond_to?(:reset_column_information)
|
36
|
+
constant_class.reset_column_information
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def teardown_defined_constants
|
42
|
+
ActiveSupport::Dependencies.clear
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
RSpec.configure do |config|
|
47
|
+
config.include ClassBuilder
|
48
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# https://github.com/thoughtbot/shoulda-matchers/blob/master/spec/support/unit/helpers/model_builder.rb
|
2
|
+
|
3
|
+
module ModelBuilder
|
4
|
+
def self.included(example_group)
|
5
|
+
example_group.class_eval do
|
6
|
+
before do
|
7
|
+
@created_tables ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
drop_created_tables
|
12
|
+
ActiveSupport::Dependencies.clear
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_table(table_name, options = {}, &block)
|
18
|
+
connection = ActiveRecord::Base.connection
|
19
|
+
|
20
|
+
begin
|
21
|
+
connection.execute("DROP TABLE IF EXISTS #{table_name}")
|
22
|
+
connection.create_table(table_name, options, &block)
|
23
|
+
@created_tables << table_name
|
24
|
+
connection
|
25
|
+
rescue Exception => e
|
26
|
+
connection.execute("DROP TABLE IF EXISTS #{table_name}")
|
27
|
+
raise e
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def define_model_class(class_name, &block)
|
32
|
+
define_class(class_name, ActiveRecord::Base, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_active_model_class(class_name, options = {}, &block)
|
36
|
+
define_class(class_name) do
|
37
|
+
include ActiveModel::Validations
|
38
|
+
|
39
|
+
options[:accessors].each do |column|
|
40
|
+
attr_accessor column.to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
if block_given?
|
44
|
+
class_eval(&block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def define_model(name, columns = {}, &block)
|
50
|
+
class_name = name.to_s.pluralize.classify
|
51
|
+
table_name = class_name.tableize
|
52
|
+
table_block = lambda do |table|
|
53
|
+
columns.each do |name, specification|
|
54
|
+
if specification.is_a?(Hash)
|
55
|
+
table.column name, specification[:type], specification[:options]
|
56
|
+
else
|
57
|
+
table.column name, specification
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if columns.key?(:id) && columns[:id] == false
|
63
|
+
columns.delete(:id)
|
64
|
+
create_table(table_name, id: false, &table_block)
|
65
|
+
else
|
66
|
+
create_table(table_name, &table_block)
|
67
|
+
end
|
68
|
+
|
69
|
+
define_model_class(class_name, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def drop_created_tables
|
73
|
+
connection = ActiveRecord::Base.connection
|
74
|
+
|
75
|
+
@created_tables.each do |table_name|
|
76
|
+
connection.execute("DROP TABLE IF EXISTS #{table_name}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
RSpec.configure do |config|
|
82
|
+
config.include ModelBuilder
|
83
|
+
end
|