Skip to content

Commit 6fdfc08

Browse files
Improve server bundle security test coverage and fix misleading comments (#1815)
This commit addresses critical gaps in test coverage and terminology issues found in the server bundle security implementation: ## Test Coverage Improvements - **Added comprehensive tests for enforce_private_server_bundles=true**: - Tests for bundle_js_file_path() with enforcement enabled - Tests for server_bundle_js_file_path() with enforcement enabled - Tests for rsc_bundle_js_file_path() with enforcement enabled - Tests verify that public directories are never checked when enforcement is enabled - **Enhanced existing test scenarios**: - Added tests for all file existence combinations when enforcement is disabled - Added proper fallback behavior tests (private -> public -> configured path) - Improved test organization with clearer context descriptions ## Bug Fixes and Code Quality - **Fixed misleading terminology**: - Changed "auto-registration" to "auto-bundling" in comments and method names - Updated method name: update_gitignore_for_auto_registration -> update_gitignore_for_generated_bundles - **Improved test reliability**: - Added proper mocking for all configuration dependencies - Used parametrized tests for comprehensive file state coverage - Added clear expectations that File.exist? should not be called on public paths when enforcement is enabled ## Security Testing The new tests ensure that the critical security feature `enforce_private_server_bundles=true` works as intended: - Server bundles are never loaded from public directories when enforcement is enabled - RSC bundles follow the same security model as server bundles - Fallback behavior is properly disabled when security enforcement is active All tests pass with 0 failures. RuboCop linting passes with 0 violations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <[email protected]>
1 parent 3c6e749 commit 6fdfc08

File tree

5 files changed

+163
-32
lines changed

5 files changed

+163
-32
lines changed

lib/generators/react_on_rails/base_generator.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def add_base_gems_to_gemfile
9595
run "bundle"
9696
end
9797

98-
def update_gitignore_for_auto_registration
98+
def update_gitignore_for_generated_bundles
9999
gitignore_path = File.join(destination_root, ".gitignore")
100100
return unless File.exist?(gitignore_path)
101101

lib/generators/react_on_rails/react_no_redux_generator.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def create_appropriate_templates
3737
component_name: "HelloWorld"
3838
}
3939

40-
# Only create the view template - no manual bundle needed for auto registration
40+
# Only create the view template - no manual bundle needed for auto-bundling
4141
template("#{base_path}/app/views/hello_world/index.html.erb.tt",
4242
"app/views/hello_world/index.html.erb", config)
4343
end

lib/generators/react_on_rails/react_with_redux_generator.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def create_appropriate_templates
6868
component_name: "HelloWorldApp"
6969
}
7070

71-
# Only create the view template - no manual bundle needed for auto registration
71+
# Only create the view template - no manual bundle needed for auto-bundling
7272
template("#{base_path}/app/views/hello_world/index.html.erb.tt",
7373
"app/views/hello_world/index.html.erb", config)
7474
end

spec/react_on_rails/support/shared_examples/react_no_redux_generator_examples.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
shared_examples "no_redux_generator" do
44
it "creates appropriate templates" do
5-
# No manual bundle for non-Redux (auto-registration only)
5+
# No manual bundle for non-Redux (auto-bundling only)
66
assert_no_file("app/javascript/packs/hello-world-bundle.js")
77

88
assert_file("app/views/hello_world/index.html.erb") do |contents|

spec/react_on_rails/utils_spec.rb

Lines changed: 159 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,9 @@ def mock_dev_server_running
146146
context "with server bundle (SSR/RSC) file not in manifest" do
147147
let(:server_bundle_name) { "server-bundle.js" }
148148
let(:ssr_generated_path) { File.expand_path(File.join("ssr-generated", server_bundle_name)) }
149+
let(:public_path) { File.expand_path(File.join(packer_public_output_path, server_bundle_name)) }
149150

150-
context "with server_bundle_output_path configured" do
151+
context "with server_bundle_output_path configured and enforce_private_server_bundles=false" do
151152
before do
152153
mock_missing_manifest_entry(server_bundle_name)
153154
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file")
@@ -158,10 +159,23 @@ def mock_dev_server_running
158159
.and_return(false)
159160
end
160161

161-
it "returns configured path directly without checking existence" do
162-
# When enforce_private_server_bundles is false, it will check File.exist?
163-
# for both private and public paths, but should still return the configured path
164-
public_path = File.expand_path(File.join(packer_public_output_path, server_bundle_name))
162+
it "returns private path when it exists even if public path also exists" do
163+
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(true)
164+
allow(File).to receive(:exist?).with(public_path).and_return(true)
165+
166+
result = described_class.bundle_js_file_path(server_bundle_name)
167+
expect(result).to eq(ssr_generated_path)
168+
end
169+
170+
it "returns public path when private path does not exist and public path exists" do
171+
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false)
172+
allow(File).to receive(:exist?).with(public_path).and_return(true)
173+
174+
result = described_class.bundle_js_file_path(server_bundle_name)
175+
expect(result).to eq(public_path)
176+
end
177+
178+
it "returns configured path if both private and public paths do not exist" do
165179
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false)
166180
allow(File).to receive(:exist?).with(public_path).and_return(false)
167181

@@ -170,6 +184,41 @@ def mock_dev_server_running
170184
end
171185
end
172186

187+
context "with server_bundle_output_path configured and enforce_private_server_bundles=true" do
188+
before do
189+
mock_missing_manifest_entry(server_bundle_name)
190+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file")
191+
.and_return(server_bundle_name)
192+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
193+
.and_return("ssr-generated")
194+
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
195+
.and_return(true)
196+
end
197+
198+
# It should always return the ssr_generated_path, regardless of which files exist
199+
file_states_combinations = [
200+
{ ssr_exists: true, public_exists: true },
201+
{ ssr_exists: true, public_exists: false },
202+
{ ssr_exists: false, public_exists: true },
203+
{ ssr_exists: false, public_exists: false }
204+
]
205+
file_states_combinations.each do |file_states|
206+
it "returns private path when enforce_private_server_bundles=true " \
207+
"(ssr_exists=#{file_states[:ssr_exists]}, " \
208+
"public_exists=#{file_states[:public_exists]})" do
209+
allow(File).to receive(:exist?)
210+
.with(ssr_generated_path)
211+
.and_return(file_states[:ssr_exists])
212+
allow(File).to receive(:exist?)
213+
.with(public_path)
214+
.and_return(file_states[:public_exists])
215+
216+
result = described_class.bundle_js_file_path(server_bundle_name)
217+
expect(result).to eq(ssr_generated_path)
218+
end
219+
end
220+
end
221+
173222
context "without server_bundle_output_path configured" do
174223
before do
175224
mock_missing_manifest_entry(server_bundle_name)
@@ -190,27 +239,65 @@ def mock_dev_server_running
190239

191240
context "with RSC bundle file not in manifest" do
192241
let(:rsc_bundle_name) { "rsc-bundle.js" }
242+
let(:public_path) { File.expand_path(File.join(packer_public_output_path, rsc_bundle_name)) }
193243
let(:ssr_generated_path) { File.expand_path(File.join("ssr-generated", rsc_bundle_name)) }
194244

195-
before do
196-
mock_missing_manifest_entry(rsc_bundle_name)
197-
allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file")
198-
.and_return(rsc_bundle_name)
199-
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
200-
.and_return("ssr-generated")
201-
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
202-
.and_return(false)
245+
context "with enforce_private_server_bundles=false" do
246+
before do
247+
mock_missing_manifest_entry(rsc_bundle_name)
248+
allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file")
249+
.and_return(rsc_bundle_name)
250+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
251+
.and_return("ssr-generated")
252+
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
253+
.and_return(false)
254+
end
255+
256+
it "returns private path when it exists even if public path also exists" do
257+
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(true)
258+
expect(File).not_to receive(:exist?).with(public_path)
259+
260+
result = described_class.bundle_js_file_path(rsc_bundle_name)
261+
expect(result).to eq(ssr_generated_path)
262+
end
263+
264+
it "fallbacks to public path when private path does not exist and public path exists" do
265+
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false)
266+
allow(File).to receive(:exist?).with(public_path).and_return(true)
267+
268+
result = described_class.bundle_js_file_path(rsc_bundle_name)
269+
expect(result).to eq(public_path)
270+
end
271+
272+
it "returns configured path if both private and public paths do not exist" do
273+
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false)
274+
allow(File).to receive(:exist?).with(public_path).and_return(false)
275+
276+
result = described_class.bundle_js_file_path(rsc_bundle_name)
277+
expect(result).to eq(ssr_generated_path)
278+
end
203279
end
204280

205-
it "treats RSC bundles as server bundles and returns configured path directly" do
206-
# When enforce_private_server_bundles is false, it will check File.exist?
207-
# for both private and public paths, but should still return the configured path
208-
public_path = File.expand_path(File.join(packer_public_output_path, rsc_bundle_name))
209-
allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false)
210-
allow(File).to receive(:exist?).with(public_path).and_return(false)
281+
context "with enforce_private_server_bundles=true" do
282+
before do
283+
mock_missing_manifest_entry(rsc_bundle_name)
284+
allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file")
285+
.and_return(rsc_bundle_name)
286+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
287+
.and_return("ssr-generated")
288+
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
289+
.and_return(true)
290+
end
291+
292+
it "enforces private RSC bundles and never checks public directory" do
293+
public_path = File.expand_path(File.join(packer_public_output_path, rsc_bundle_name))
294+
295+
# Should not check public path when enforcement is enabled
296+
expect(File).not_to receive(:exist?).with(public_path)
211297

212-
result = described_class.bundle_js_file_path(rsc_bundle_name)
213-
expect(result).to eq(ssr_generated_path)
298+
result = described_class.bundle_js_file_path(rsc_bundle_name)
299+
expect(result).to eq(ssr_generated_path)
300+
end
214301
end
215302
end
216303
end
@@ -263,7 +350,7 @@ def mock_dev_server_running
263350
expect(path).to end_with("ssr-generated/#{server_bundle_name}")
264351
end
265352

266-
context "with server_bundle_output_path configured" do
353+
context "with server_bundle_output_path configured and enforce_private_server_bundles=false" do
267354
it "returns the configured path directly without checking file existence" do
268355
server_bundle_name = "server-bundle.js"
269356
mock_bundle_configs(server_bundle_name: server_bundle_name)
@@ -283,6 +370,26 @@ def mock_dev_server_running
283370
expect(path).to end_with("ssr-generated/#{server_bundle_name}")
284371
end
285372
end
373+
374+
context "with server_bundle_output_path configured and enforce_private_server_bundles=true" do
375+
it "returns the private path without checking public directories" do
376+
server_bundle_name = "server-bundle.js"
377+
mock_missing_manifest_entry(server_bundle_name)
378+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file")
379+
.and_return(server_bundle_name)
380+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
381+
.and_return("ssr-generated")
382+
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
383+
.and_return(true)
384+
385+
# Should not check public paths when enforcement is enabled
386+
public_path = File.expand_path(File.join(packer_public_output_path, server_bundle_name))
387+
expect(File).not_to receive(:exist?).with(public_path)
388+
389+
path = described_class.server_bundle_js_file_path
390+
expect(path).to end_with("ssr-generated/#{server_bundle_name}")
391+
end
392+
end
286393
end
287394

288395
context "with server file in the manifest, used for client", packer_type.to_sym do
@@ -346,14 +453,38 @@ def mock_dev_server_running
346453
include_context "with #{packer_type} enabled"
347454

348455
context "with server file not in manifest", packer_type.to_sym do
349-
it "returns the private ssr-generated path for RSC bundles" do
350-
server_bundle_name = "rsc-bundle.js"
351-
mock_bundle_configs(rsc_bundle_name: server_bundle_name)
352-
mock_missing_manifest_entry(server_bundle_name)
456+
context "with enforce_private_server_bundles=false" do
457+
it "returns the private ssr-generated path for RSC bundles" do
458+
server_bundle_name = "rsc-bundle.js"
459+
mock_bundle_configs(rsc_bundle_name: server_bundle_name)
460+
mock_missing_manifest_entry(server_bundle_name)
353461

354-
path = described_class.rsc_bundle_js_file_path
462+
path = described_class.rsc_bundle_js_file_path
355463

356-
expect(path).to end_with("ssr-generated/#{server_bundle_name}")
464+
expect(path).to end_with("ssr-generated/#{server_bundle_name}")
465+
end
466+
end
467+
468+
context "with enforce_private_server_bundles=true" do
469+
it "returns the private ssr-generated path for RSC bundles without checking public paths" do
470+
server_bundle_name = "rsc-bundle.js"
471+
mock_missing_manifest_entry(server_bundle_name)
472+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file")
473+
.and_return("server-bundle.js") # Different from RSC bundle name
474+
allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file")
475+
.and_return(server_bundle_name)
476+
allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path")
477+
.and_return("ssr-generated")
478+
allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles")
479+
.and_return(true)
480+
481+
# Should not check public paths when enforcement is enabled
482+
public_path = File.expand_path(File.join(packer_public_output_path, server_bundle_name))
483+
expect(File).not_to receive(:exist?).with(public_path)
484+
485+
path = described_class.rsc_bundle_js_file_path
486+
expect(path).to end_with("ssr-generated/#{server_bundle_name}")
487+
end
357488
end
358489
end
359490

0 commit comments

Comments
 (0)