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