bullet 6.1.2 → 7.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +82 -0
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile.rails-7.0 +10 -0
  5. data/README.md +10 -7
  6. data/lib/bullet/active_record41.rb +1 -0
  7. data/lib/bullet/active_record42.rb +1 -0
  8. data/lib/bullet/active_record52.rb +22 -17
  9. data/lib/bullet/active_record60.rb +22 -17
  10. data/lib/bullet/active_record61.rb +22 -17
  11. data/lib/bullet/active_record70.rb +261 -0
  12. data/lib/bullet/bullet_xhr.js +1 -0
  13. data/lib/bullet/dependency.rb +10 -0
  14. data/lib/bullet/detector/base.rb +2 -1
  15. data/lib/bullet/detector/counter_cache.rb +1 -1
  16. data/lib/bullet/detector/n_plus_one_query.rb +3 -3
  17. data/lib/bullet/detector/unused_eager_loading.rb +1 -1
  18. data/lib/bullet/mongoid4x.rb +1 -1
  19. data/lib/bullet/mongoid5x.rb +1 -1
  20. data/lib/bullet/mongoid6x.rb +1 -1
  21. data/lib/bullet/mongoid7x.rb +24 -7
  22. data/lib/bullet/notification.rb +2 -1
  23. data/lib/bullet/rack.rb +5 -3
  24. data/lib/bullet/stack_trace_filter.rb +7 -9
  25. data/lib/bullet/version.rb +1 -1
  26. data/lib/bullet.rb +68 -22
  27. data/perf/benchmark.rb +4 -1
  28. data/spec/bullet/detector/unused_eager_loading_spec.rb +6 -2
  29. data/spec/bullet/ext/object_spec.rb +1 -1
  30. data/spec/bullet/rack_spec.rb +88 -17
  31. data/spec/bullet_spec.rb +39 -10
  32. data/spec/integration/active_record/association_spec.rb +53 -8
  33. data/spec/integration/counter_cache_spec.rb +4 -4
  34. data/spec/integration/mongoid/association_spec.rb +1 -1
  35. data/spec/models/attachment.rb +5 -0
  36. data/spec/models/deal.rb +5 -0
  37. data/spec/models/folder.rb +2 -1
  38. data/spec/models/group.rb +2 -1
  39. data/spec/models/page.rb +2 -1
  40. data/spec/models/post.rb +2 -0
  41. data/spec/models/submission.rb +1 -0
  42. data/spec/models/user.rb +1 -0
  43. data/spec/models/writer.rb +2 -1
  44. data/spec/spec_helper.rb +0 -2
  45. data/spec/support/mongo_seed.rb +1 -0
  46. data/spec/support/sqlite_seed.rb +20 -0
  47. data/test.sh +2 -0
  48. metadata +10 -4
  49. data/.travis.yml +0 -33
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Bullet
4
4
  module Detector
5
- class Base; end
5
+ class Base
6
+ end
6
7
  end
7
8
  end
@@ -54,7 +54,7 @@ module Bullet
54
54
  private
55
55
 
56
56
  def create_notification(klazz, associations)
57
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:counter_cache, klazz)
57
+ notify_associations = Array(associations) - Bullet.get_safelist_associations(:counter_cache, klazz)
58
58
 
59
59
  if notify_associations.present?
60
60
  notice = Bullet::Notification::CounterCache.new klazz, notify_associations
@@ -35,6 +35,7 @@ module Bullet
35
35
 
36
36
  objects = Array(object_or_objects)
37
37
  return if objects.map(&:bullet_primary_key_value).compact.empty?
38
+ return if objects.all? { |obj| obj.class.name =~ /^HABTM_/ }
38
39
 
39
40
  Bullet.debug(
40
41
  'Detector::NPlusOneQuery#add_possible_objects',
@@ -84,8 +85,7 @@ module Bullet
84
85
  # associations == v comparison order is important here because
85
86
  # v variable might be a squeel node where :== method is redefined,
86
87
  # so it does not compare values at all and return unexpected results
87
- result =
88
- v.is_a?(Hash) ? v.key?(associations) : associations == v
88
+ result = v.is_a?(Hash) ? v.key?(associations) : associations == v
89
89
  return true if result
90
90
  end
91
91
 
@@ -95,7 +95,7 @@ module Bullet
95
95
  private
96
96
 
97
97
  def create_notification(callers, klazz, associations)
98
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:n_plus_one_query, klazz)
98
+ notify_associations = Array(associations) - Bullet.get_safelist_associations(:n_plus_one_query, klazz)
99
99
 
100
100
  if notify_associations.present?
101
101
  notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
@@ -65,7 +65,7 @@ module Bullet
65
65
  private
66
66
 
67
67
  def create_notification(callers, klazz, associations)
68
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:unused_eager_loading, klazz)
68
+ notify_associations = Array(associations) - Bullet.get_safelist_associations(:unused_eager_loading, klazz)
69
69
 
70
70
  if notify_associations.present?
71
71
  notice = Bullet::Notification::UnusedEagerLoading.new(callers, klazz, notify_associations)
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -25,14 +25,31 @@ module Bullet
25
25
  def each(&block)
26
26
  return to_enum unless block_given?
27
27
 
28
- records = []
29
- origin_each { |record| records << record }
30
- if records.length > 1
31
- Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
32
- elsif records.size == 1
33
- Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
28
+ first_document = nil
29
+ document_count = 0
30
+
31
+ origin_each do |document|
32
+ document_count += 1
33
+
34
+ if document_count == 1
35
+ first_document = document
36
+ elsif document_count == 2
37
+ Bullet::Detector::NPlusOneQuery.add_possible_objects([first_document, document])
38
+ yield(first_document)
39
+ first_document = nil
40
+ yield(document)
41
+ else
42
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(document)
43
+ yield(document)
44
+ end
34
45
  end
35
- records.each(&block)
46
+
47
+ if document_count == 1
48
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(first_document)
49
+ yield(first_document)
50
+ end
51
+
52
+ self
36
53
  end
37
54
 
38
55
  def eager_load(docs)
@@ -7,6 +7,7 @@ module Bullet
7
7
  autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query'
8
8
  autoload :CounterCache, 'bullet/notification/counter_cache'
9
9
 
10
- class UnoptimizedQueryError < StandardError; end
10
+ class UnoptimizedQueryError < StandardError
11
+ end
11
12
  end
12
13
  end
data/lib/bullet/rack.rb CHANGED
@@ -22,9 +22,11 @@ module Bullet
22
22
  response_body = response_body(response)
23
23
  response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
24
24
  response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
25
- response_body = append_to_html_body(response_body, xhr_script)
25
+ if Bullet.add_footer && !Bullet.skip_http_headers
26
+ response_body = append_to_html_body(response_body, xhr_script)
27
+ end
26
28
  headers['Content-Length'] = response_body.bytesize.to_s
27
- else
29
+ elsif !Bullet.skip_http_headers
28
30
  set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
29
31
  set_header(headers, 'X-bullet-console-text', Bullet.text_notifications) if Bullet.console_enabled?
30
32
  end
@@ -82,7 +84,7 @@ module Bullet
82
84
  def response_body(response)
83
85
  if response.respond_to?(:body)
84
86
  Array === response.body ? response.body.first : response.body
85
- else
87
+ elsif response.respond_to?(:first)
86
88
  response.first
87
89
  end
88
90
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
+ require "bundler"
2
3
 
3
4
  module Bullet
4
5
  module StackTraceFilter
5
6
  VENDOR_PATH = '/vendor'
7
+ IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
6
8
 
7
9
  def caller_in_project
8
10
  vendor_root = Bullet.app_root + VENDOR_PATH
@@ -10,8 +12,9 @@ module Bullet
10
12
  select_caller_locations do |location|
11
13
  caller_path = location_as_path(location)
12
14
  caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
13
- !caller_path.include?(bundler_path) ||
14
- Bullet.stacktrace_includes.any? { |include_pattern| pattern_matches?(location, include_pattern) }
15
+ !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
16
+ pattern_matches?(location, include_pattern)
17
+ }
15
18
  end
16
19
  end
17
20
 
@@ -47,20 +50,15 @@ module Bullet
47
50
  end
48
51
 
49
52
  def location_as_path(location)
50
- ruby_19? ? location : location.absolute_path.to_s
53
+ IS_RUBY_19 ? location : location.absolute_path.to_s
51
54
  end
52
55
 
53
56
  def select_caller_locations
54
- if ruby_19?
57
+ if IS_RUBY_19
55
58
  caller.select { |caller_path| yield caller_path }
56
59
  else
57
60
  caller_locations.select { |location| yield location }
58
61
  end
59
62
  end
60
-
61
- def ruby_19?
62
- @ruby_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') if @ruby_19.nil?
63
- @ruby_19
64
- end
65
63
  end
66
64
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '6.1.2'
4
+ VERSION = '7.0.0'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -20,9 +20,6 @@ module Bullet
20
20
  autoload :Registry, 'bullet/registry'
21
21
  autoload :NotificationCollector, 'bullet/notification_collector'
22
22
 
23
- BULLET_DEBUG = 'BULLET_DEBUG'
24
- TRUE = 'true'
25
-
26
23
  if defined?(Rails::Railtie)
27
24
  class BulletRailtie < Rails::Railtie
28
25
  initializer 'bullet.configure_rails_initialization' do |app|
@@ -38,10 +35,11 @@ module Bullet
38
35
  :stacktrace_includes,
39
36
  :stacktrace_excludes,
40
37
  :skip_html_injection
41
- attr_reader :whitelist
42
- attr_accessor :add_footer, :orm_patches_applied
38
+ attr_reader :safelist
39
+ attr_accessor :add_footer, :orm_patches_applied, :skip_http_headers
43
40
 
44
- available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.map { |notifier| "#{notifier}=" }
41
+ available_notifiers =
42
+ UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }.map { |notifier| "#{notifier}=" }
45
43
  available_notifiers_options = { to: UniformNotifier }
46
44
  delegate(*available_notifiers, **available_notifiers_options)
47
45
 
@@ -59,7 +57,7 @@ module Bullet
59
57
  @enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable
60
58
 
61
59
  if enable?
62
- reset_whitelist
60
+ reset_safelist
63
61
  unless orm_patches_applied
64
62
  self.orm_patches_applied = true
65
63
  Bullet::Mongoid.enable if mongoid?
@@ -72,8 +70,9 @@ module Bullet
72
70
  !!@enable
73
71
  end
74
72
 
73
+ # Rails.root might be nil if `railties` is a dependency on a project that does not use Rails
75
74
  def app_root
76
- (defined?(::Rails.root) ? Rails.root.to_s : Dir.pwd).to_s
75
+ @app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s
77
76
  end
78
77
 
79
78
  def n_plus_one_query_enable?
@@ -89,36 +88,81 @@ module Bullet
89
88
  end
90
89
 
91
90
  def stacktrace_includes
92
- @stacktrace_includes || []
91
+ @stacktrace_includes ||= []
93
92
  end
94
93
 
95
94
  def stacktrace_excludes
96
- @stacktrace_excludes || []
95
+ @stacktrace_excludes ||= []
96
+ end
97
+
98
+ def add_safelist(options)
99
+ reset_safelist
100
+ @safelist[options[:type]][options[:class_name]] ||= []
101
+ @safelist[options[:type]][options[:class_name]] << options[:association].to_sym
102
+ end
103
+
104
+ def delete_safelist(options)
105
+ reset_safelist
106
+ @safelist[options[:type]][options[:class_name]] ||= []
107
+ @safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
108
+ @safelist[options[:type]].delete_if { |_key, val| val.empty? }
109
+ end
110
+
111
+ def get_safelist_associations(type, class_name)
112
+ Array(@safelist[type][class_name])
113
+ end
114
+
115
+ def reset_safelist
116
+ @safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
117
+ end
118
+
119
+ def clear_safelist
120
+ @safelist = nil
97
121
  end
98
122
 
99
123
  def add_whitelist(options)
100
- reset_whitelist
101
- @whitelist[options[:type]][options[:class_name]] ||= []
102
- @whitelist[options[:type]][options[:class_name]] << options[:association].to_sym
124
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
125
+ add_whitelist is deprecated in favor of add_safelist. It will be removed from the next major release.
126
+ WARN
127
+ )
128
+
129
+ add_safelist(options)
103
130
  end
104
131
 
105
132
  def delete_whitelist(options)
106
- reset_whitelist
107
- @whitelist[options[:type]][options[:class_name]] ||= []
108
- @whitelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
109
- @whitelist[options[:type]].delete_if { |_key, val| val.empty? }
133
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
134
+ delete_whitelist is deprecated in favor of delete_safelist. It will be removed from the next major release.
135
+ WARN
136
+ )
137
+
138
+ delete_safelist(options)
110
139
  end
111
140
 
112
141
  def get_whitelist_associations(type, class_name)
113
- Array(@whitelist[type][class_name])
142
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
143
+ get_whitelist_associations is deprecated in favor of get_safelist_associations. It will be removed from the next major release.
144
+ WARN
145
+ )
146
+
147
+ get_safelist_associations(type, class_name)
114
148
  end
115
149
 
116
150
  def reset_whitelist
117
- @whitelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
151
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
152
+ reset_whitelist is deprecated in favor of reset_safelist. It will be removed from the next major release.
153
+ WARN
154
+ )
155
+
156
+ reset_safelist
118
157
  end
119
158
 
120
159
  def clear_whitelist
121
- @whitelist = nil
160
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
161
+ clear_whitelist is deprecated in favor of clear_safelist. It will be removed from the next major release.
162
+ WARN
163
+ )
164
+
165
+ clear_safelist
122
166
  end
123
167
 
124
168
  def bullet_logger=(active)
@@ -132,7 +176,7 @@ module Bullet
132
176
  end
133
177
 
134
178
  def debug(title, message)
135
- puts "[Bullet][#{title}] #{message}" if ENV[BULLET_DEBUG] == TRUE
179
+ puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true'
136
180
  end
137
181
 
138
182
  def start_request
@@ -241,7 +285,9 @@ module Bullet
241
285
  end
242
286
 
243
287
  def inject_into_page?
244
- !@skip_html_injection && (console_enabled? || add_footer)
288
+ return false if defined?(@skip_html_injection) && @skip_html_injection
289
+
290
+ console_enabled? || add_footer
245
291
  end
246
292
 
247
293
  private
data/perf/benchmark.rb CHANGED
@@ -30,7 +30,10 @@ end
30
30
 
31
31
  # create database bullet_benchmark;
32
32
  ActiveRecord::Base.establish_connection(
33
- adapter: 'mysql2', database: 'bullet_benchmark', server: '/tmp/mysql.socket', username: 'root'
33
+ adapter: 'mysql2',
34
+ database: 'bullet_benchmark',
35
+ server: '/tmp/mysql.socket',
36
+ username: 'root'
34
37
  )
35
38
 
36
39
  ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
@@ -19,7 +19,9 @@ module Bullet
19
19
  it 'should get call associations if object and association are both in eager_loadings and call_object_associations' do
20
20
  UnusedEagerLoading.add_eager_loadings([@post], :association)
21
21
  UnusedEagerLoading.add_call_object_associations(@post, :association)
22
- expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq([:association])
22
+ expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq(
23
+ [:association]
24
+ )
23
25
  end
24
26
 
25
27
  it 'should not get call associations if not exist in call_object_associations' do
@@ -30,7 +32,9 @@ module Bullet
30
32
 
31
33
  context '.diff_object_associations' do
32
34
  it 'should return associations not exist in call_association' do
33
- expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq([:association])
35
+ expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq(
36
+ [:association]
37
+ )
34
38
  end
35
39
 
36
40
  it 'should return empty if associations exist in call_association' do
@@ -10,7 +10,7 @@ describe Object do
10
10
  end
11
11
 
12
12
  if mongoid?
13
- it 'should return class with namesapce and id composition' do
13
+ it 'should return class with namespace and id composition' do
14
14
  post = Mongoid::Post.first
15
15
  expect(post.bullet_key).to eq("Mongoid::Post:#{post.id}")
16
16
  end
@@ -69,7 +69,6 @@ module Bullet
69
69
  expect(Bullet).to receive(:notification?).and_return(true)
70
70
  expect(Bullet).to receive(:console_enabled?).and_return(true)
71
71
  expect(Bullet).to receive(:gather_inline_notifications).and_return('<bullet></bullet>')
72
- expect(middleware).to receive(:xhr_script).and_return('')
73
72
  expect(Bullet).to receive(:perform_out_of_channel_notifications)
74
73
  _, headers, response = middleware.call('Content-Type' => 'text/html')
75
74
  expect(headers['Content-Length']).to eq('56')
@@ -84,37 +83,44 @@ module Bullet
84
83
  allow(Bullet).to receive(:console_enabled?).and_return(true)
85
84
  expect(Bullet).to receive(:gather_inline_notifications).and_return('<bullet></bullet>')
86
85
  _, headers, response = middleware.call('Content-Type' => 'text/html')
87
- expect(headers['Content-Length']).to eq((58 + middleware.send(:xhr_script).length).to_s)
86
+ expect(headers['Content-Length']).to eq('58')
88
87
  end
89
88
 
90
89
  context 'with injection notifiers' do
91
90
  before do
92
91
  expect(Bullet).to receive(:notification?).and_return(true)
93
92
  allow(Bullet).to receive(:gather_inline_notifications).and_return('<bullet></bullet>')
94
- allow(middleware).to receive(:xhr_script).and_return('')
93
+ allow(middleware).to receive(:xhr_script).and_return('<script></script>')
95
94
  allow(middleware).to receive(:footer_note).and_return('footer')
96
95
  expect(Bullet).to receive(:perform_out_of_channel_notifications)
97
96
  end
98
97
 
99
98
  it 'should change response body if add_footer is true' do
100
- expect(Bullet).to receive(:add_footer).twice.and_return(true)
99
+ expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true)
101
100
  _, headers, response = middleware.call('Content-Type' => 'text/html')
102
101
 
103
- expect(headers['Content-Length']).to eq((56 + middleware.send(:footer_note).length).to_s)
104
- expect(response.first).to start_with('<html><head></head><body>')
105
- expect(response.first).to include('<bullet></bullet><')
102
+ expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s)
103
+ expect(response).to eq(%w[<html><head></head><body>footer<bullet></bullet><script></script></body></html>])
106
104
  end
107
105
 
108
106
  it 'should change response body for html safe string if add_footer is true' do
109
- expect(Bullet).to receive(:add_footer).twice.and_return(true)
110
- app.response = Support::ResponseDouble.new.tap do |response|
111
- response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
112
- end
107
+ expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true)
108
+ app.response =
109
+ Support::ResponseDouble.new.tap do |response|
110
+ response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
111
+ end
113
112
  _, headers, response = middleware.call('Content-Type' => 'text/html')
114
113
 
115
- expect(headers['Content-Length']).to eq((56 + middleware.send(:footer_note).length).to_s)
116
- expect(response.first).to start_with('<html><head></head><body>')
117
- expect(response.first).to include('<bullet></bullet><')
114
+ expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s)
115
+ expect(response).to eq(%w[<html><head></head><body>footer<bullet></bullet><script></script></body></html>])
116
+ end
117
+
118
+ it 'should add the footer-text header for non-html requests when add_footer is true' do
119
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
120
+ allow(Bullet).to receive(:footer_info).and_return(['footer text'])
121
+ app.headers = { 'Content-Type' => 'application/json' }
122
+ _, headers, _response = middleware.call({})
123
+ expect(headers).to include('X-bullet-footer-text' => '["footer text"]')
118
124
  end
119
125
 
120
126
  it 'should change response body if console_enabled is true' do
@@ -126,20 +132,71 @@ module Bullet
126
132
 
127
133
  it 'should change response body for html safe string if console_enabled is true' do
128
134
  expect(Bullet).to receive(:console_enabled?).and_return(true)
129
- app.response = Support::ResponseDouble.new.tap do |response|
130
- response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
131
- end
135
+ app.response =
136
+ Support::ResponseDouble.new.tap do |response|
137
+ response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
138
+ end
132
139
  _, headers, response = middleware.call('Content-Type' => 'text/html')
133
140
  expect(headers['Content-Length']).to eq('56')
134
141
  expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
135
142
  end
136
143
 
144
+ it 'should add headers for non-html requests when console_enabled is true' do
145
+ allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
146
+ allow(Bullet).to receive(:text_notifications).and_return(['text notifications'])
147
+ app.headers = { 'Content-Type' => 'application/json' }
148
+ _, headers, _response = middleware.call({})
149
+ expect(headers).to include('X-bullet-console-text' => '["text notifications"]')
150
+ end
151
+
137
152
  it "shouldn't change response body unnecessarily" do
138
153
  expected_response = Support::ResponseDouble.new 'Actual body'
139
154
  app.response = expected_response
140
155
  _, _, response = middleware.call({})
141
156
  expect(response).to eq(expected_response)
142
157
  end
158
+
159
+ it "shouldn't add headers unnecessarily" do
160
+ app.headers = { 'Content-Type' => 'application/json' }
161
+ _, headers, _response = middleware.call({})
162
+ expect(headers).not_to include('X-bullet-footer-text')
163
+ expect(headers).not_to include('X-bullet-console-text')
164
+ end
165
+
166
+ context 'when skip_http_headers is enabled' do
167
+ before do
168
+ allow(Bullet).to receive(:skip_http_headers).and_return(true)
169
+ end
170
+
171
+ it 'should include the footer but not the xhr script tag if add_footer is true' do
172
+ expect(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
173
+ _, headers, response = middleware.call({})
174
+
175
+ expect(headers['Content-Length']).to eq((56 + middleware.send(:footer_note).length).to_s)
176
+ expect(response).to eq(%w[<html><head></head><body>footer<bullet></bullet></body></html>])
177
+ end
178
+
179
+ it 'should not include the xhr script tag if console_enabled is true' do
180
+ expect(Bullet).to receive(:console_enabled?).and_return(true)
181
+ _, headers, response = middleware.call({})
182
+ expect(headers['Content-Length']).to eq('56')
183
+ expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
184
+ end
185
+
186
+ it 'should not add the footer-text header for non-html requests when add_footer is true' do
187
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
188
+ app.headers = { 'Content-Type' => 'application/json' }
189
+ _, headers, _response = middleware.call({})
190
+ expect(headers).not_to include('X-bullet-footer-text')
191
+ end
192
+
193
+ it 'should not add headers for non-html requests when console_enabled is true' do
194
+ allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
195
+ app.headers = { 'Content-Type' => 'application/json' }
196
+ _, headers, _response = middleware.call({})
197
+ expect(headers).not_to include('X-bullet-console-text')
198
+ end
199
+ end
143
200
  end
144
201
 
145
202
  context 'when skip_html_injection is enabled' do
@@ -204,6 +261,20 @@ module Bullet
204
261
  expect(middleware.response_body(response)).to eq body_string
205
262
  end
206
263
  end
264
+
265
+ begin
266
+ require 'rack/files'
267
+
268
+ context 'when `response` is a Rack::Files::Iterator' do
269
+ let(:response) { instance_double(::Rack::Files::Iterator) }
270
+ before { allow(response).to receive(:is_a?).with(::Rack::Files::Iterator) { true } }
271
+
272
+ it 'should return nil' do
273
+ expect(middleware.response_body(response)).to be_nil
274
+ end
275
+ end
276
+ rescue LoadError
277
+ end
207
278
  end
208
279
  end
209
280
  end
data/spec/bullet_spec.rb CHANGED
@@ -74,31 +74,60 @@ describe Bullet, focused: true do
74
74
  end
75
75
  end
76
76
 
77
+ describe '#add_safelist' do
78
+ context "for 'special' class names" do
79
+ it 'is added to the safelist successfully' do
80
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
81
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
82
+ end
83
+ end
84
+ end
85
+
77
86
  describe '#add_whitelist' do
78
87
  context "for 'special' class names" do
79
- it 'is added to the whitelist successfully' do
88
+ it 'is added to the safelist successfully' do
80
89
  Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
81
- expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to include :department
90
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
91
+ end
92
+ end
93
+ end
94
+
95
+ describe '#delete_safelist' do
96
+ context "for 'special' class names" do
97
+ it 'is deleted from the safelist successfully' do
98
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
99
+ Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
100
+ expect(Bullet.safelist[:n_plus_one_query]).to eq({})
101
+ end
102
+ end
103
+
104
+ context 'when exists multiple definitions' do
105
+ it 'is deleted from the safelist successfully' do
106
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
107
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
108
+ Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
109
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
110
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
82
111
  end
83
112
  end
84
113
  end
85
114
 
86
115
  describe '#delete_whitelist' do
87
116
  context "for 'special' class names" do
88
- it 'is deleted from the whitelist successfully' do
89
- Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
117
+ it 'is deleted from the safelist successfully' do
118
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
90
119
  Bullet.delete_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
91
- expect(Bullet.whitelist[:n_plus_one_query]).to eq({})
120
+ expect(Bullet.safelist[:n_plus_one_query]).to eq({})
92
121
  end
93
122
  end
94
123
 
95
124
  context 'when exists multiple definitions' do
96
- it 'is deleted from the whitelist successfully' do
97
- Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
98
- Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
125
+ it 'is deleted from the safelist successfully' do
126
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
127
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
99
128
  Bullet.delete_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
100
- expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to include :department
101
- expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
129
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
130
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
102
131
  end
103
132
  end
104
133
  end