Skip to content

Commit ff544ab

Browse files
Improve server bundle security test coverage and fix misleading comments
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 ff544ab

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)