bullet 7.0.3 → 7.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad1f81292cc89778a41c6dd178bbfb6342fe1b69c4e57c3fdad0d8003d97b525
4
- data.tar.gz: 5c1c8113f6e03ecdb449bce5e12273b969f29c323a5bb802c13c0539ea439a69
3
+ metadata.gz: f67f5d4add512b0606aa27cf26e9d2a628d6eb7dc58078d4c3eaeca4d3bfccf7
4
+ data.tar.gz: aa55cdd15e0f585a679595b98b858194c3d217acedf9ef0db70085cc74b7710c
5
5
  SHA512:
6
- metadata.gz: 1f0d4d73910f96ab8c6ae5b6b1bc58de547c2c32a7c689c2984778465dae29d2c2cae81c23699b3848d2eaef30223db66bcca6ccb421b9078d5616f497421d45
7
- data.tar.gz: 3b9001966918da22bfe7b2960526ac0db471a75e79e7939b099bebe3dde1fb27ea69b1afbdec0fcc98a6cb4260b845b5551a1a07cec036207c885be4e14ef7b9
6
+ metadata.gz: 919bb619038a60d8657bc09f2c34949934c933de190ae0d3ed2025e4599e6cbe284b132b2ed92ea6f94dfbc06566ba56aa522684107cab1aa3a1386a431a527e
7
+ data.tar.gz: 3682a26c40d5a6b6559f7d82324ad3236ba91db28a99f1416117a5147986e26cdc75c643a0a0a58891f781ba5511a44d90124f5f7a637ce43b2d3c3e31baf3a5
@@ -9,9 +9,9 @@ name: CI
9
9
 
10
10
  on:
11
11
  push:
12
- branches: [ master ]
12
+ branches: [ main ]
13
13
  pull_request:
14
- branches: [ master ]
14
+ branches: [ main ]
15
15
 
16
16
  jobs:
17
17
  test_rails_4:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## Next Release
2
2
 
3
+ ## 7.0.5 (01/01/2023)
4
+
5
+ * Fix n+1 false positives in AR 7.0
6
+ * Fix eager_load nested has_many :through false positives
7
+ * Respect Content-Security-Policy nonces
8
+ * Added CallStacks support for avoid eager loading
9
+ * Iterate fewer times over objects
10
+
11
+ ## 7.0.4 (11/28/2022)
12
+
13
+ * Fix `eager_load` `has_many :through` false positives
14
+ * mongoid7x: add dynamic methods
15
+
3
16
  ## 7.0.3 (08/13/2022)
4
17
 
5
18
  * Replace `Array()` with `Array.wrap()`
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com)
1
+ Copyright (c) 2009 - 2022 Richard Huang (flyerhzm@gmail.com)
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -222,7 +222,7 @@ end
222
222
 
223
223
  ### Work with sinatra
224
224
 
225
- Configure and use `Bullet::Rack`
225
+ Configure and use `Bullet::Rack`.
226
226
 
227
227
  ```ruby
228
228
  configure :development do
@@ -232,6 +232,8 @@ configure :development do
232
232
  end
233
233
  ```
234
234
 
235
+ If your application generates a Content-Security-Policy via a separate middleware, ensure that `Bullet::Rack` is loaded _before_ that middleware.
236
+
235
237
  ### Run in tests
236
238
 
237
239
  First you need to enable Bullet in test environment.
@@ -285,7 +287,7 @@ $ rails g scaffold comment name:string post_id:integer
285
287
  $ bundle exec rake db:migrate
286
288
  ```
287
289
 
288
- 2\. Change `app/model/post.rb` and `app/model/comment.rb`
290
+ 2\. Change `app/models/post.rb` and `app/models/comment.rb`
289
291
 
290
292
  ```ruby
291
293
  class Post < ActiveRecord::Base
@@ -177,16 +177,18 @@ module Bullet
177
177
  if Bullet.start?
178
178
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
179
179
  refl = reflection.through_reflection
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
181
- association = owner.association refl.name
182
- Array.wrap(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
180
+ association = owner.association(refl.name)
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if refl.through_reflection?
187
- refl = refl.through_reflection while refl.through_reflection?
187
+ if refl.through_reflection?
188
+ refl = refl.through_reflection while refl.through_reflection?
188
189
 
189
- Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
190
+ Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
191
+ end
190
192
  end
191
193
  end
192
194
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -150,14 +150,16 @@ module Bullet
150
150
 
151
151
  if Bullet.start?
152
152
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
153
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
154
- association = owner.association reflection.through_reflection.name
155
- Array.wrap(association.target).each do |through_record|
156
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
157
- end
153
+ association = owner.association(reflection.through_reflection.name)
154
+ if association.loaded?
155
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
156
+ Array.wrap(association.target).each do |through_record|
157
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
158
+ end
158
159
 
159
- if reflection.through_reflection != through_reflection
160
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
160
+ if reflection.through_reflection != through_reflection
161
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
162
+ end
161
163
  end
162
164
  end
163
165
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -177,14 +177,16 @@ module Bullet
177
177
 
178
178
  if Bullet.start?
179
179
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
181
180
  association = owner.association(reflection.through_reflection.name)
182
- Array.wrap(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if reflection.through_reflection != through_reflection
187
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
187
+ if reflection.through_reflection != through_reflection
188
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
189
+ end
188
190
  end
189
191
  end
190
192
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -177,14 +177,16 @@ module Bullet
177
177
 
178
178
  if Bullet.start?
179
179
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
181
180
  association = owner.association(reflection.through_reflection.name)
182
- Array.wrap(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if reflection.through_reflection != through_reflection
187
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
187
+ if reflection.through_reflection != through_reflection
188
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
189
+ end
188
190
  end
189
191
  end
190
192
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -170,6 +170,13 @@ module Bullet
170
170
  end
171
171
  super
172
172
  end
173
+
174
+ def inversed_from_queries(record)
175
+ if Bullet.start? && inversable?(record)
176
+ Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
177
+ end
178
+ super
179
+ end
173
180
  end
174
181
  )
175
182
 
@@ -180,14 +187,16 @@ module Bullet
180
187
 
181
188
  if Bullet.start?
182
189
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
183
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
184
190
  association = owner.association(reflection.through_reflection.name)
185
- Array.wrap(association.target).each do |through_record|
186
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
187
- end
191
+ if association.loaded?
192
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
193
+ Array.wrap(association.target).each do |through_record|
194
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
195
+ end
188
196
 
189
- if reflection.through_reflection != through_reflection
190
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
197
+ if reflection.through_reflection != through_reflection
198
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
199
+ end
191
200
  end
192
201
  end
193
202
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
@@ -13,6 +13,7 @@ module Bullet
13
13
  'Detector::Association#add_object_associations',
14
14
  "object: #{object.bullet_key}, associations: #{associations}"
15
15
  )
16
+ call_stacks.add(object.bullet_key)
16
17
  object_associations.add(object.bullet_key, associations)
17
18
  end
18
19
 
@@ -25,6 +26,7 @@ module Bullet
25
26
  'Detector::Association#add_call_object_associations',
26
27
  "object: #{object.bullet_key}, associations: #{associations}"
27
28
  )
29
+ call_stacks.add(object.bullet_key)
28
30
  call_object_associations.add(object.bullet_key, associations)
29
31
  end
30
32
 
@@ -76,6 +78,12 @@ module Bullet
76
78
  def eager_loadings
77
79
  Thread.current[:bullet_eager_loadings]
78
80
  end
81
+
82
+ # cal_stacks keeps stacktraces where querie-objects were called from.
83
+ # e.g. { 'Object:111' => [SomeProject/app/controllers/...] }
84
+ def call_stacks
85
+ Thread.current[:bullet_call_stacks]
86
+ end
79
87
  end
80
88
  end
81
89
  end
@@ -25,7 +25,7 @@ module Bullet
25
25
  )
26
26
  if !excluded_stacktrace_path? && conditions_met?(object, associations)
27
27
  Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}")
28
- create_notification caller_in_project, object.class.to_s, associations
28
+ create_notification caller_in_project(object.bullet_key), object.class.to_s, associations
29
29
  end
30
30
  end
31
31
 
@@ -34,14 +34,25 @@ module Bullet
34
34
  return unless Bullet.n_plus_one_query_enable?
35
35
 
36
36
  objects = Array.wrap(object_or_objects)
37
- return if objects.map(&:bullet_primary_key_value).compact.empty?
38
- return if objects.all? { |obj| obj.class.name =~ /^HABTM_/ }
39
-
40
- Bullet.debug(
41
- 'Detector::NPlusOneQuery#add_possible_objects',
42
- "objects: #{objects.map(&:bullet_key).join(', ')}"
43
- )
44
- objects.each { |object| possible_objects.add object.bullet_key }
37
+ class_names_match_regex = true
38
+ primary_key_values_are_empty = true
39
+ keys_joined = ""
40
+ objects.each do |obj|
41
+ unless obj.class.name =~ /^HABTM_/
42
+ class_names_match_regex = false
43
+ end
44
+ unless obj.bullet_primary_key_value.nil?
45
+ primary_key_values_are_empty = false
46
+ end
47
+ keys_joined += "#{(keys_joined.empty?? '' : ', ')}#{obj.bullet_key}"
48
+ end
49
+ unless class_names_match_regex || primary_key_values_are_empty
50
+ Bullet.debug(
51
+ 'Detector::NPlusOneQuery#add_possible_objects',
52
+ "objects: #{keys_joined}"
53
+ )
54
+ objects.each { |object| possible_objects.add object.bullet_key }
55
+ end
45
56
  end
46
57
 
47
58
  def add_impossible_object(object)
@@ -20,7 +20,7 @@ module Bullet
20
20
  next if object_association_diff.empty?
21
21
 
22
22
  Bullet.debug('detect unused preload', "object: #{bullet_key}, associations: #{object_association_diff}")
23
- create_notification(caller_in_project, bullet_key.bullet_class_name, object_association_diff)
23
+ create_notification(caller_in_project(bullet_key), bullet_key.bullet_class_name, object_association_diff)
24
24
  end
25
25
  end
26
26
 
@@ -4,22 +4,20 @@ module Bullet
4
4
  module Mongoid
5
5
  def self.enable
6
6
  require 'mongoid'
7
+ require 'rubygems'
7
8
  ::Mongoid::Contextual::Mongo.class_eval do
8
9
  alias_method :origin_first, :first
9
10
  alias_method :origin_last, :last
10
11
  alias_method :origin_each, :each
11
12
  alias_method :origin_eager_load, :eager_load
12
13
 
13
- def first(opts = {})
14
- result = origin_first(opts)
15
- Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
16
- result
17
- end
18
-
19
- def last(opts = {})
20
- result = origin_last(opts)
21
- Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
22
- result
14
+ %i[first last].each do |context|
15
+ default = Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new('7.5') ? nil : {}
16
+ define_method(context) do |opts = default|
17
+ result = send(:"origin_#{context}", opts)
18
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
19
+ result
20
+ end
23
21
  end
24
22
 
25
23
  def each(&block)
data/lib/bullet/rack.rb CHANGED
@@ -4,6 +4,8 @@ module Bullet
4
4
  class Rack
5
5
  include Dependency
6
6
 
7
+ NONCE_MATCHER = /script-src .*'nonce-(?<nonce>[A-Za-z0-9+\/]+={0,2})'/
8
+
7
9
  def initialize(app)
8
10
  @app = app
9
11
  end
@@ -20,11 +22,15 @@ module Bullet
20
22
  if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
21
23
  if html_request?(headers, response)
22
24
  response_body = response_body(response)
23
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
24
- response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
25
- if Bullet.add_footer && !Bullet.skip_http_headers
26
- response_body = append_to_html_body(response_body, xhr_script)
25
+
26
+ with_security_policy_nonce(headers) do |nonce|
27
+ response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
28
+ response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
29
+ if Bullet.add_footer && !Bullet.skip_http_headers
30
+ response_body = append_to_html_body(response_body, xhr_script(nonce))
31
+ end
27
32
  end
33
+
28
34
  headers['Content-Length'] = response_body.bytesize.to_s
29
35
  elsif !Bullet.skip_http_headers
30
36
  set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
@@ -118,8 +124,34 @@ module Bullet
118
124
  end
119
125
 
120
126
  # Make footer work for XHR requests by appending data to the footer
121
- def xhr_script
122
- "<script type='text/javascript'>#{File.read("#{__dir__}/bullet_xhr.js")}</script>"
127
+ def xhr_script(nonce = nil)
128
+ script = File.read("#{__dir__}/bullet_xhr.js")
129
+
130
+ if nonce
131
+ "<script type='text/javascript' nonce='#{nonce}'>#{script}</script>"
132
+ else
133
+ "<script type='text/javascript'>#{script}</script>"
134
+ end
135
+ end
136
+
137
+ def with_security_policy_nonce(headers)
138
+ matched = (headers['Content-Security-Policy'] || '').match(NONCE_MATCHER)
139
+ nonce = matched[:nonce] if matched
140
+
141
+ if nonce
142
+ console_enabled = UniformNotifier.console
143
+ alert_enabled = UniformNotifier.alert
144
+
145
+ UniformNotifier.console = { attributes: { nonce: nonce } } if console_enabled
146
+ UniformNotifier.alert = { attributes: { nonce: nonce } } if alert_enabled
147
+
148
+ yield nonce
149
+
150
+ UniformNotifier.console = console_enabled
151
+ UniformNotifier.alert = alert_enabled
152
+ else
153
+ yield
154
+ end
123
155
  end
124
156
  end
125
157
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module Registry
5
+ class CallStack < Base
6
+ # remembers found association backtrace
7
+ def add(key)
8
+ @registry[key] = Thread.current.backtrace
9
+ end
10
+ end
11
+ end
12
+ end
@@ -5,5 +5,6 @@ module Bullet
5
5
  autoload :Base, 'bullet/registry/base'
6
6
  autoload :Object, 'bullet/registry/object'
7
7
  autoload :Association, 'bullet/registry/association'
8
+ autoload :CallStack, 'bullet/registry/call_stack'
8
9
  end
9
10
  end
@@ -6,10 +6,11 @@ module Bullet
6
6
  VENDOR_PATH = '/vendor'
7
7
  IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
8
8
 
9
- def caller_in_project
9
+ # @param bullet_key[String] - use this to get stored call stack from call_stacks object.
10
+ def caller_in_project(bullet_key = nil)
10
11
  vendor_root = Bullet.app_root + VENDOR_PATH
11
12
  bundler_path = Bundler.bundle_path.to_s
12
- select_caller_locations do |location|
13
+ select_caller_locations(bullet_key) do |location|
13
14
  caller_path = location_as_path(location)
14
15
  caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
15
16
  !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
@@ -50,15 +51,16 @@ module Bullet
50
51
  end
51
52
 
52
53
  def location_as_path(location)
54
+ return location if location.is_a?(String)
55
+
53
56
  IS_RUBY_19 ? location : location.absolute_path.to_s
54
57
  end
55
58
 
56
- def select_caller_locations
57
- if IS_RUBY_19
58
- caller.select { |caller_path| yield caller_path }
59
- else
60
- caller_locations.select { |location| yield location }
61
- end
59
+ def select_caller_locations(bullet_key = nil)
60
+ return caller.select { |caller_path| yield caller_path } if IS_RUBY_19
61
+
62
+ call_stack = bullet_key ? call_stacks[bullet_key] : caller_locations
63
+ call_stack.select { |location| yield location }
62
64
  end
63
65
  end
64
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '7.0.3'
4
+ VERSION = '7.0.5'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -23,7 +23,11 @@ module Bullet
23
23
  if defined?(Rails::Railtie)
24
24
  class BulletRailtie < Rails::Railtie
25
25
  initializer 'bullet.configure_rails_initialization' do |app|
26
- app.middleware.use Bullet::Rack
26
+ if defined?(ActionDispatch::ContentSecurityPolicy::Middleware)
27
+ app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
28
+ else
29
+ app.middleware.use Bullet::Rack
30
+ end
27
31
  end
28
32
  end
29
33
  end
@@ -144,6 +148,7 @@ module Bullet
144
148
  Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new
145
149
  Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new
146
150
  Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new
151
+ Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new
147
152
 
148
153
  Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new
149
154
  Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new
@@ -127,38 +127,6 @@ module Bullet
127
127
  end
128
128
  end
129
129
 
130
- context '.caller_in_project' do
131
- it 'should include only paths that are in the project' do
132
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
133
- not_in_project = OpenStruct.new(absolute_path: '/def/def.rb')
134
-
135
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, not_in_project])
136
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
137
- expect(NPlusOneQuery).to receive(:create_notification).with([in_project], 'Post', :association)
138
- NPlusOneQuery.call_association(@post, :association)
139
- end
140
-
141
- context 'stacktrace_includes' do
142
- before { Bullet.stacktrace_includes = ['def', /xyz/] }
143
- after { Bullet.stacktrace_includes = nil }
144
-
145
- it 'should include paths that are in the stacktrace_include list' do
146
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
147
- included_gems = [OpenStruct.new(absolute_path: '/def/def.rb'), OpenStruct.new(absolute_path: 'xyz/xyz.rb')]
148
- excluded_gem = OpenStruct.new(absolute_path: '/ghi/ghi.rb')
149
-
150
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, *included_gems, excluded_gem])
151
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
152
- expect(NPlusOneQuery).to receive(:create_notification).with(
153
- [in_project, *included_gems],
154
- 'Post',
155
- :association
156
- )
157
- NPlusOneQuery.call_association(@post, :association)
158
- end
159
- end
160
- end
161
-
162
130
  context '.add_possible_objects' do
163
131
  it 'should add possible objects' do
164
132
  NPlusOneQuery.add_possible_objects([@post, @post2])
@@ -65,6 +65,11 @@ module Bullet
65
65
  expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association])
66
66
  UnusedEagerLoading.check_unused_preload_associations
67
67
  end
68
+
69
+ it 'should create call stack for notification' do
70
+ UnusedEagerLoading.add_object_associations(@post, :association)
71
+ expect(UnusedEagerLoading.send(:call_stacks).registry).not_to be_empty
72
+ end
68
73
  end
69
74
 
70
75
  context '.add_eager_loadings' do
@@ -50,7 +50,7 @@ module Bullet
50
50
  end
51
51
 
52
52
  it 'should be true if no response body' do
53
- response = double()
53
+ response = double
54
54
  expect(middleware).to be_empty(response)
55
55
  end
56
56
  end
@@ -129,6 +129,23 @@ module Bullet
129
129
  expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
130
130
  end
131
131
 
132
+ it 'should include CSP nonce in inline script if console_enabled and a CSP is applied' do
133
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
134
+ expect(Bullet).to receive(:console_enabled?).and_return(true)
135
+ allow(middleware).to receive(:xhr_script).and_call_original
136
+
137
+ nonce = '+t9/wTlgG6xbHxXYUaDNzQ=='
138
+ app.headers = {
139
+ 'Content-Type' => 'text/html',
140
+ 'Content-Security-Policy' => "default-src 'self' https:; script-src 'self' https: 'nonce-#{nonce}'"
141
+ }
142
+
143
+ _, headers, response = middleware.call('Content-Type' => 'text/html')
144
+
145
+ size = 56 + middleware.send(:footer_note).length + middleware.send(:xhr_script, nonce).length
146
+ expect(headers['Content-Length']).to eq(size.to_s)
147
+ end
148
+
132
149
  it 'should change response body for html safe string if console_enabled is true' do
133
150
  expect(Bullet).to receive(:console_enabled?).and_return(true)
134
151
  app.response =
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Bullet
6
+ RSpec.describe StackTraceFilter do
7
+ let(:dummy_class) { Class.new { extend StackTraceFilter } }
8
+ let(:root_path) { Dir.pwd }
9
+ let(:bundler_path) { Bundler.bundle_path }
10
+
11
+ describe '#caller_in_project' do
12
+ it 'gets the caller in the project' do
13
+ expect(dummy_class).to receive(:call_stacks).and_return({
14
+ 'Post:1' => [
15
+ File.join(root_path, 'lib/bullet.rb'),
16
+ File.join(root_path, 'vendor/uniform_notifier.rb'),
17
+ File.join(bundler_path, 'rack.rb')
18
+ ]
19
+ })
20
+ expect(dummy_class.caller_in_project('Post:1')).to eq([
21
+ File.join(root_path, 'lib/bullet.rb')
22
+ ])
23
+ end
24
+ end
25
+ end
26
+ end
@@ -58,6 +58,19 @@ if active_record?
58
58
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
59
59
  end
60
60
 
61
+ it 'should detect non preload comment => post with inverse_of from a query' do
62
+ Post.first.comments.find_each do |comment|
63
+ comment.name
64
+ comment.post.name
65
+ end
66
+
67
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
68
+ expect(Post.first.comments.count).not_to eq(0)
69
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
70
+
71
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
72
+ end
73
+
61
74
  it 'should detect non preload post => comments with empty?' do
62
75
  Post.all.each { |post| post.comments.empty? }
63
76
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
@@ -474,7 +487,15 @@ if active_record?
474
487
  end
475
488
 
476
489
  it 'should detect preload associations' do
477
- Firm.includes(:clients).each { |firm| firm.clients.map(&:name) }
490
+ Firm.preload(:clients).each { |firm| firm.clients.map(&:name) }
491
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
492
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
493
+
494
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
495
+ end
496
+
497
+ it 'should detect eager load association' do
498
+ Firm.eager_load(:clients).each { |firm| firm.clients.map(&:name) }
478
499
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
479
500
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
480
501
 
@@ -508,7 +529,15 @@ if active_record?
508
529
  end
509
530
 
510
531
  it 'should detect preload associations' do
511
- Firm.includes(:groups).each { |firm| firm.groups.map(&:name) }
532
+ Firm.preload(:groups).each { |firm| firm.groups.map(&:name) }
533
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
534
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
535
+
536
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
537
+ end
538
+
539
+ it 'should detect eager load associations' do
540
+ Firm.eager_load(:groups).each { |firm| firm.groups.map(&:name) }
512
541
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
513
542
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
514
543
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.3
4
+ version: 7.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-13 00:00:00.000000000 Z
11
+ date: 2023-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -104,6 +104,7 @@ files:
104
104
  - lib/bullet/registry.rb
105
105
  - lib/bullet/registry/association.rb
106
106
  - lib/bullet/registry/base.rb
107
+ - lib/bullet/registry/call_stack.rb
107
108
  - lib/bullet/registry/object.rb
108
109
  - lib/bullet/stack_trace_filter.rb
109
110
  - lib/bullet/version.rb
@@ -126,6 +127,7 @@ files:
126
127
  - spec/bullet/registry/association_spec.rb
127
128
  - spec/bullet/registry/base_spec.rb
128
129
  - spec/bullet/registry/object_spec.rb
130
+ - spec/bullet/stack_trace_filter_spec.rb
129
131
  - spec/bullet_spec.rb
130
132
  - spec/integration/active_record/association_spec.rb
131
133
  - spec/integration/counter_cache_spec.rb
@@ -195,7 +197,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
197
  - !ruby/object:Gem::Version
196
198
  version: 1.3.6
197
199
  requirements: []
198
- rubygems_version: 3.3.7
200
+ rubygems_version: 3.4.1
199
201
  signing_key:
200
202
  specification_version: 4
201
203
  summary: help to kill N+1 queries and unused eager loading.
@@ -216,6 +218,7 @@ test_files:
216
218
  - spec/bullet/registry/association_spec.rb
217
219
  - spec/bullet/registry/base_spec.rb
218
220
  - spec/bullet/registry/object_spec.rb
221
+ - spec/bullet/stack_trace_filter_spec.rb
219
222
  - spec/bullet_spec.rb
220
223
  - spec/integration/active_record/association_spec.rb
221
224
  - spec/integration/counter_cache_spec.rb