shopify_dashboard_plus 0.0.7 → 1.0.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +16 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile.lock +99 -35
  5. data/README.md +16 -7
  6. data/Rakefile +13 -0
  7. data/bin/shopify_dashboard_plus.rb +1 -1
  8. data/config.ru +4 -0
  9. data/lib/shopify_dashboard_plus.rb +40 -52
  10. data/lib/shopify_dashboard_plus/currency.rb +31 -0
  11. data/lib/shopify_dashboard_plus/discount_report.rb +36 -0
  12. data/lib/shopify_dashboard_plus/helpers.rb +65 -51
  13. data/lib/shopify_dashboard_plus/report.rb +4 -196
  14. data/lib/shopify_dashboard_plus/revenue_report.rb +55 -0
  15. data/lib/shopify_dashboard_plus/sales_report.rb +86 -0
  16. data/lib/shopify_dashboard_plus/traffic_report.rb +65 -0
  17. data/lib/shopify_dashboard_plus/version.rb +3 -1
  18. data/shopify_dashboard_plus.gemspec +17 -7
  19. data/test/fixtures/vcr_cassettes/.gitkeep +0 -0
  20. data/test/fixtures/vcr_cassettes/authenticate.yml +88 -0
  21. data/test/fixtures/vcr_cassettes/multiple_pages_orders.yml +1544 -0
  22. data/test/fixtures/vcr_cassettes/orders_from_2010_01_01.yml +815 -0
  23. data/test/fixtures/vcr_cassettes/orders_from_2010_01_01_to_2015_01_01.yml +566 -0
  24. data/test/fixtures/vcr_cassettes/orders_no_paramaters.yml +81 -0
  25. data/test/fixtures/vcr_cassettes/orders_none.yml +77 -0
  26. data/test/fixtures/vcr_cassettes/orders_to_2015-06-26.yml +81 -0
  27. data/test/resources/anonymizer.rb +125 -0
  28. data/test/resources/modify_data.rb +110 -0
  29. data/test/strip_sensitive_data.rb +79 -0
  30. data/test/test_app.rb +60 -0
  31. data/test/test_frontend.rb +76 -0
  32. data/test/test_mockdata.rb +237 -0
  33. data/views/connect.erb +6 -6
  34. data/views/layout.erb +2 -3
  35. data/views/report.erb +1 -1
  36. metadata +190 -59
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'YAML'
4
+ require 'json'
5
+ require 'faker'
6
+ require_relative 'resources/modify_data'
7
+ require_relative 'resources/anonymizer'
8
+
9
+
10
+ def strip_shop_details(cassette_name:)
11
+ include ModifyData
12
+ include Anonymizer
13
+
14
+ # Load shop details authentication VCR cassette
15
+ request_data = YAML.load_file "test/fixtures/vcr_cassettes/#{cassette_name}"
16
+ response_payloads = request_data['http_interactions']
17
+
18
+ # Iterate through all requests saved in cassette
19
+ # Replace sensitive shop information with randomly generated information
20
+ response_payloads.each_with_index do |payload, index|
21
+ anonymous_shop = ModifyData.anonymize_shop(payload['response']['body']['string'])
22
+ response_payloads[index]['response']['body']['string'] = anonymous_shop
23
+ end
24
+
25
+ # Propagate new information to full request payload in YAML file
26
+ request_data['http_interactions'] = response_payloads
27
+ File.open("test/fixtures/vcr_cassettes/#{cassette_name}", 'w') { |f| YAML.dump(request_data, f) }
28
+ end
29
+
30
+
31
+ def strip_order_details(cassette_name:)
32
+ include ModifyData
33
+ include Anonymizer
34
+
35
+ # Load orders
36
+ request_data = YAML.load_file "test/fixtures/vcr_cassettes/#{cassette_name}"
37
+ response_payloads = request_data['http_interactions']
38
+
39
+ # Iterate through all requests saved in cassette
40
+ # Replace all sensitive order data with randomly generated information
41
+ response_payloads.each_with_index do |payload, index|
42
+ anonymous_order = ModifyData.anonymize_orders(payload['response']['body']['string'])
43
+ response_payloads[index]['response']['body']['string'] = anonymous_order
44
+ end
45
+
46
+ # Propagate new information to full request payload in YAML file
47
+ request_data['http_interactions'] = response_payloads
48
+ File.open("test/fixtures/vcr_cassettes/#{cassette_name}", 'w') { |f| YAML.dump(request_data, f) }
49
+ end
50
+
51
+
52
+ def duplicate_orders(cassette_name:, multiplier:, output_cassette:)
53
+ include ModifyData
54
+ include Anonymizer
55
+
56
+ # Load orders
57
+ request_data = YAML.load_file "test/fixtures/vcr_cassettes/#{cassette_name}"
58
+ response_payloads = request_data['http_interactions']
59
+
60
+ # Iterate through all requests saved in cassette
61
+ # Replace all sensitive order data with randomly generated information
62
+ response_payloads.each_with_index do |payload, index|
63
+ duplicated_order_list = ModifyData.duplicate_orders(payload['response']['body']['string'], multiplier_constant: multiplier)
64
+ response_payloads[index]['response']['body']['string'] = duplicated_order_list
65
+ end
66
+
67
+ # Propagate new information to full request payload in YAML file
68
+ request_data['http_interactions'] = response_payloads
69
+ File.open("test/fixtures/vcr_cassettes/#{output_cassette}", 'w') { |f| YAML.dump(request_data, f) }
70
+ end
71
+
72
+
73
+ strip_shop_details(cassette_name: 'authenticate.yml')
74
+ strip_order_details(cassette_name: 'orders_from_2010_01_01_to_2015_01_01.yml')
75
+ strip_order_details(cassette_name: 'orders_from_2010_01_01.yml')
76
+ strip_order_details(cassette_name: 'orders_no_paramaters.yml')
77
+ strip_order_details(cassette_name: 'orders_to_2015-06-26.yml')
78
+ duplicate_orders(cassette_name: 'orders_from_2010_01_01_to_2015_01_01.yml', multiplier: 3, output_cassette: 'multiple_pages_orders.yml')
79
+
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'rack/test'
5
+ require 'tilt/erb'
6
+ require 'vcr'
7
+ require 'webmock'
8
+ require './lib/shopify_dashboard_plus.rb'
9
+ require './lib/shopify_dashboard_plus/version'
10
+ require './lib/shopify_dashboard_plus/helpers'
11
+ require './lib/shopify_dashboard_plus/discount_report'
12
+ require './lib/shopify_dashboard_plus/revenue_report'
13
+ require './lib/shopify_dashboard_plus/sales_report'
14
+ require './lib/shopify_dashboard_plus/traffic_report'
15
+
16
+ ENV['RACK_ENV'] = 'test'
17
+
18
+ ## Application tests related to:
19
+ ## Launching application, File Structure, and Version
20
+
21
+
22
+ class TestShopifyDashboardPlus < MiniTest::Test
23
+ include Rack::Test::Methods
24
+
25
+ # VCR from test_mockdata test suite should not intercept these HTTP requests
26
+ VCR.turned_off do
27
+
28
+ # Allow real HTTP Requests
29
+ WebMock.allow_net_connect!
30
+
31
+ def app
32
+ Capybara.app = Sinatra::Application
33
+ end
34
+
35
+ def test_version_exists
36
+ # Should be formatted as A.B.C (A, B, C all integers) Ex. => 2.1.3
37
+ # Ensure the portion before the last decimal converts to an integer (A.B). Ex => 2.1.to_i
38
+ @version = ShopifyDashboardPlus::VERSION
39
+ assert @version.gsub(/.\d\Z/, "").to_i, "#{@version} does not seem to be formated as X.Y.Z where X, Y, Z are integers"
40
+ end
41
+
42
+ def test_directories_exist
43
+ assert_equal(true, File.directory?("./bin"), "bin directory does not exist!")
44
+ assert_equal(true, File.directory?("./lib"), "lib directory does not exist!")
45
+ assert_equal(true, File.directory?("./lib/shopify_dashboard_plus"), "lib/shopify_dashboard_plus directory does not exist!")
46
+ assert_equal(true, File.directory?("./public"), "public directory does not exist!")
47
+ assert_equal(true, File.directory?("./public/css"), "public/css directory does not exist!")
48
+ assert_equal(true, File.directory?("./public/js"), "public/js directory does not exist!")
49
+ assert_equal(true, File.directory?("./test"), "test directory does not exist!")
50
+ assert_equal(true, File.directory?("./test/fixtures"), "test/fixtures directory does not exist!")
51
+ assert_equal(true, File.directory?("./views"), "views directory does not exist!")
52
+ end
53
+
54
+ def test_launch_sinatra
55
+ # Script returns true for zero exit status, false for non-zero
56
+ assert true, `ruby "./bin/shopify_dashboard_plus.rb"`
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'rack/test'
5
+ require 'capybara'
6
+ require 'capybara/dsl'
7
+ require 'capybara/webkit'
8
+ require 'tilt/erb'
9
+ require 'vcr'
10
+ require 'webmock'
11
+ require './lib/shopify_dashboard_plus.rb'
12
+ require './lib/shopify_dashboard_plus/version'
13
+ require './lib/shopify_dashboard_plus/helpers'
14
+ require './lib/shopify_dashboard_plus/discount_report'
15
+ require './lib/shopify_dashboard_plus/revenue_report'
16
+ require './lib/shopify_dashboard_plus/sales_report'
17
+ require './lib/shopify_dashboard_plus/traffic_report'
18
+
19
+ ENV['RACK_ENV'] = 'test'
20
+
21
+ ## Application tests related to:
22
+ ## Front-end testing (flash errors messages, etc) using Capybara-Webkit
23
+
24
+
25
+ class TestShopifyDashboardPlus < MiniTest::Test
26
+ include Rack::Test::Methods
27
+ include Capybara::DSL
28
+
29
+ # VCR from test_mockdata test suite should not intercept these HTTP requests
30
+ VCR.turned_off do
31
+
32
+ # Allow real HTTP Requests
33
+ WebMock.allow_net_connect!
34
+
35
+ # Necessary to use capybara with sinatra applicaiton
36
+ Capybara.app = Sinatra::Application
37
+
38
+
39
+ def setup
40
+ Capybara.configure do |config|
41
+ config.run_server = false
42
+ config.current_driver = :webkit
43
+ config.default_driver = :webkit
44
+ config.javascript_driver = :webkit
45
+ config.page.driver.browser.ignore_ssl_errors
46
+ config.default_wait_time = 10
47
+ config.always_include_port = true
48
+ config.server_port = 31_337
49
+ config.app_host = "http://127.0.0.1"
50
+ end
51
+ end
52
+
53
+ def invalid_connection(api_key: nil, api_pwd: nil, shop_name: nil)
54
+ assert_equal("/connect", page.current_path)
55
+ within '#connect-box' do
56
+ fill_in 'api_key', :with => api_key if api_key
57
+ fill_in 'api_pwd', :with => api_pwd if api_pwd
58
+ fill_in 'shop_name', :with => shop_name if shop_name
59
+ click_button 'connect'
60
+ end
61
+ assert_equal 'Failed to Connect...', find('p#flash').text
62
+ end
63
+
64
+ def test_invalid_credentials_flash_message
65
+ page.visit '/connect'
66
+ invalid_connection(api_key: "bad_api_key")
67
+ page.visit '/connect'
68
+ invalid_connection(api_pwd: "bad_api_pwd")
69
+ page.visit '/connect'
70
+ invalid_connection(shop_name: "bad_shop_name")
71
+ page.visit '/connect'
72
+ invalid_connection
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'rack/test'
5
+ require 'tilt/erb'
6
+ require 'capybara'
7
+ require 'vcr'
8
+ require 'byebug'
9
+ require './lib/shopify_dashboard_plus.rb'
10
+ require './lib/shopify_dashboard_plus/version'
11
+ require './lib/shopify_dashboard_plus/helpers'
12
+ require './lib/shopify_dashboard_plus/discount_report'
13
+ require './lib/shopify_dashboard_plus/revenue_report'
14
+ require './lib/shopify_dashboard_plus/sales_report'
15
+ require './lib/shopify_dashboard_plus/traffic_report'
16
+
17
+ ENV['RACK_ENV'] = 'test'
18
+
19
+ VCR.configure do |config|
20
+ config.cassette_library_dir = 'test/fixtures/vcr_cassettes'
21
+
22
+ # Shopify Gem uses Net::HTTP
23
+ # Requests can be captured using webmock
24
+ config.hook_into :webmock
25
+ config.ignore_hosts '127.0.0.1', 'localhost', '0.0.0.0', 'example.com'
26
+ # config.debug_logger = File.open('request_log.log', 'w')
27
+
28
+ # Allow other test suites to send real HTTP requests
29
+ config.allow_http_connections_when_no_cassette = true
30
+
31
+ # Show response payload in body as plaintext and hide API credentials
32
+ config.before_record { |cassette| cassette.response.body.force_encoding('UTF-8') }
33
+ config.filter_sensitive_data('<API_KEY>') { ENV['API_KEY'] || "testkey" }
34
+ config.filter_sensitive_data('<API_PWD>') { ENV['API_PWD'] || "testpwd" }
35
+ config.filter_sensitive_data('<SHOP_NAME>') { ENV['SHOP_NAME'] || "testshop" }
36
+
37
+ # Match certain requests by date in the payload
38
+ config.register_request_matcher :port do |req1, req2|
39
+ @today == VCR::HTTPInteraction
40
+ end
41
+ end
42
+
43
+
44
+ class TestShopifyDashboardPlus < MiniTest::Test
45
+ include Rack::Test::Methods
46
+
47
+ attr_accessor :authenticated
48
+
49
+ def app
50
+ Sinatra::Application
51
+ end
52
+
53
+ def setup
54
+ @today = DateTime.now.strftime('%Y-%m-%d')
55
+ @hardcoded_day = "2015-06-26"
56
+ end
57
+
58
+
59
+ #######################
60
+ ## Common Methods
61
+ #######################
62
+
63
+ def env_set?
64
+ true if ENV['API_KEY'] && ENV['API_PWD'] && ENV['SHOP_NAME']
65
+ end
66
+
67
+ def authenticate
68
+ return if authenticated
69
+
70
+ VCR.use_cassette('authenticate', :match_requests_on => [:path]) do
71
+
72
+ # Use environment variables if specified, otherwise use fake information
73
+ # Shopify URL will appear as <api_key>:<api_pwd>@<storename>.myshopify.com/<path>
74
+ # VCR casettes should match on the URI path, not the host information
75
+ payload = if env_set?
76
+ "api_key=#{ENV['API_KEY']}&api_pwd=#{ENV['API_PWD']}&shop_name=#{ENV['SHOP_NAME']}"
77
+ else
78
+ "api_key=testkey&api_pwd=testpwd&shop_name=testshop"
79
+ end
80
+
81
+ post('/connect', payload, "Content-Type" => "application/x-www-form-urlencoded")
82
+ end
83
+ end
84
+
85
+ def build_url(from: nil, to: nil)
86
+ if from && to
87
+ "/?from=#{from}&to=#{to}"
88
+ elsif from
89
+ "/?from=#{from}"
90
+ elsif to
91
+ "/?to=#{to}"
92
+ else
93
+ "/"
94
+ end
95
+ end
96
+
97
+ def validate_body(returned_body)
98
+ # No Three digit precision decimal points
99
+ assert_equal 0, returned_body.scan(/[0-9]{1,}[.][0-9]{3,}/).length
100
+
101
+ # Time / Times is pluralized correctly
102
+ assert_equal 0, returned_body.scan(/1 Times/).length
103
+ assert_equal 0, returned_body.scan(/[02-9] Time^[s]/).length
104
+
105
+ # Referral / Referrals is pluralized correctly
106
+ assert_equal 0, returned_body.scan(/1 Referrals/).length
107
+ assert_equal 0, returned_body.scan(/[02-9] Referral^[s]/).length
108
+
109
+ # Valid Response
110
+ assert_match(/Retrieve metrics over the following period/, returned_body)
111
+ assert_equal 0, returned_body.scan(/Invalid Dates. Please use format YYYY-MM-DD/).length
112
+ end
113
+
114
+
115
+ #######################
116
+ ## Test Cases
117
+ #######################
118
+
119
+ def test_unauthorized_redirect
120
+ VCR.turned_off do
121
+ get '/?from=2013-01-01&to=2015-01-01'
122
+ follow_redirect!
123
+ assert_match(/connect/, last_request.fullpath)
124
+ end
125
+ end
126
+
127
+ def test_no_parameters
128
+ # Validate results with no start date or end date parameter set
129
+ # Results should default to today's results
130
+ return unless env_set?
131
+
132
+ authenticate
133
+ url = build_url
134
+
135
+ # Will reuse cassette for tests run the same day (in which the URL paramater created_at_min=YYYY-MM-DD will be identical)
136
+ # Will append a new entry on a new day
137
+ VCR.use_cassette(:orders_no_paramaters, :erb => { :today => @today }, :record => :once, :match_requests_on => [:method]) do
138
+ r = get url
139
+ assert_equal last_request.fullpath, '/'
140
+
141
+ # Ensure default start and end date are today's date
142
+ assert_equal 2, r.body.scan(/placeholder=\"#{@today}\"/).length
143
+ validate_body(r.body)
144
+ end
145
+ end
146
+
147
+
148
+ def test_only_start_date_parameter
149
+ # Validate results with only start date parameter set
150
+ # End date should default to today
151
+ authenticate
152
+ url = build_url(:from => "2010-01-01")
153
+
154
+ VCR.use_cassette(:orders_from_2010_01_01, :record => :once, :match_requests_on => [:path]) do
155
+ r = get url
156
+ assert_equal '/?from=2010-01-01', last_request.fullpath
157
+ validate_body(r.body)
158
+ end
159
+ end
160
+
161
+
162
+ def test_only_end_date_parameter
163
+ # Validate results with only the end date parameter set
164
+ authenticate
165
+ url = build_url(:to => @hardcoded_day)
166
+
167
+ VCR.use_cassette("orders_to_#{@hardcoded_day}", :match_requests_on => [:path]) do
168
+ r = get url
169
+ assert_equal "/?to=#{@hardcoded_day}", last_request.fullpath
170
+ validate_body(r.body)
171
+ end
172
+ end
173
+
174
+
175
+ def test_valid_date_range
176
+ # Validate results over a valid start date and end date
177
+ authenticate
178
+ url = build_url(:from => "2010-01-01", :to => "2015-01-01")
179
+
180
+ VCR.use_cassette(:orders_from_2010_01_01_to_2015_01_01, :match_requests_on => [:path]) do
181
+ r = get url
182
+
183
+ assert_equal '/?from=2010-01-01&to=2015-01-01', last_request.fullpath
184
+ validate_body(r.body)
185
+ end
186
+ end
187
+
188
+
189
+ def test_pagination
190
+ # Validate that more than 250 results can be returned (the limit)
191
+ authenticate
192
+ url = build_url(:from => "2010-01-01", :to => "2015-01-01")
193
+
194
+ VCR.use_cassette(:multiple_pages_orders, :match_requests_on => [:path]) do
195
+ r = get url
196
+
197
+ sales_regexp = r.body.scan(/(Number of Sales[<][\/]h5>)\n(.*[<]h3[ ]class=["]money["][>][0-9]*)/).first[1]
198
+ number_of_sales = sales_regexp.scan(/\d+$/).first
199
+
200
+ assert number_of_sales.to_i > 250, "number of sales (#{number_of_sales}) does not encompass at least two pages (> 250 entries)"
201
+ end
202
+ end
203
+
204
+
205
+ def test_empty_order_set
206
+ # Validate an empty order is rendered correctly
207
+ authenticate
208
+ url = build_url
209
+
210
+ VCR.use_cassette(:orders_none, :match_requests_on => [:path]) do
211
+ r = get url
212
+
213
+ validate_body(r.body)
214
+ end
215
+ end
216
+
217
+
218
+ #######################
219
+ # Negative Test Cases
220
+ #######################
221
+
222
+ def test_end_date_before_start_date
223
+ authenticate
224
+ url = build_url(:from => "2015-01-01", :to => "2010-01-01")
225
+
226
+ get url
227
+ assert_match(/Invalid Dates. Please use format YYYY-MM-DD/, last_response.body)
228
+ end
229
+
230
+ def test_unsupported_date_characters
231
+ authenticate
232
+ url = build_url(:from => "abcd", :to => "fghijk")
233
+
234
+ get url
235
+ assert_match(/Invalid Dates. Please use format YYYY-MM-DD/, last_response.body)
236
+ end
237
+ end