ruact 0.0.10 → 0.0.12
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 +4 -4
- data/CHANGELOG.md +123 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +16 -6
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -1
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "active_support/concern"
|
|
5
|
-
# Re-run-5 (2026-05-15) — the `:current_user` inherited-helper clobber
|
|
6
|
-
# test creates a `Class.new(ActionController::Base)`, which requires
|
|
7
|
-
# `action_controller` to be loaded. Pre-Re-run-5 this test was the
|
|
8
|
-
# first thing in the suite to demand-load `action_controller`,
|
|
9
|
-
# which had the side effect of triggering `Rails::Application`'s
|
|
10
|
-
# class definition AFTER spec_helper's `require "ruact"` ran — so
|
|
11
|
-
# `Ruact::Railtie` never got loaded into the process and downstream
|
|
12
|
-
# specs that depend on the Railtie's `to_prepare` hook would behave
|
|
13
|
-
# inconsistently. Loading both explicitly here makes the order
|
|
14
|
-
# deterministic.
|
|
15
|
-
require "action_controller"
|
|
16
|
-
require "ruact"
|
|
17
|
-
require "ruact/controller"
|
|
18
|
-
|
|
19
|
-
module Ruact
|
|
20
|
-
RSpec.describe Controller do
|
|
21
|
-
let(:test_class) do
|
|
22
|
-
Class.new do
|
|
23
|
-
include Ruact::Controller
|
|
24
|
-
|
|
25
|
-
attr_reader :request
|
|
26
|
-
|
|
27
|
-
def initialize(fake_request)
|
|
28
|
-
@request = fake_request
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Minimal test double — `headers`, `format`, and `accepts` needed for the
|
|
34
|
-
# methods under test. `format` must be a public struct member (not
|
|
35
|
-
# Kernel#format) so that verify_partial_doubles can stub it. Story 10.0:
|
|
36
|
-
# `#default_render` now keys off `request.accepts` (HTML acceptability), so
|
|
37
|
-
# `accepts` is a settable member too.
|
|
38
|
-
let(:fake_request) { Struct.new(:headers, :format, :accepts).new({}, nil, []) }
|
|
39
|
-
let(:controller) { test_class.new(fake_request) }
|
|
40
|
-
|
|
41
|
-
describe "#ruact_manifest" do
|
|
42
|
-
# Opt OUT of spec_helper's suite-wide resolver stub here so this proves the
|
|
43
|
-
# actual delegation: `#ruact_manifest` resolves through ManifestResolver
|
|
44
|
-
# (prod returns the boot-loaded Ruact.manifest; dev fetches over HTTP).
|
|
45
|
-
it "delegates manifest resolution to ManifestResolver.resolve" do
|
|
46
|
-
test_manifest = ClientManifest.from_hash({})
|
|
47
|
-
allow(ManifestResolver).to receive(:resolve).and_return(test_manifest)
|
|
48
|
-
|
|
49
|
-
expect(controller.send(:ruact_manifest)).to be test_manifest
|
|
50
|
-
expect(ManifestResolver).to have_received(:resolve)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
describe "#ruact_request?" do
|
|
55
|
-
it "returns true when Accept: text/x-component" do
|
|
56
|
-
fake_request.headers["Accept"] = "text/x-component"
|
|
57
|
-
expect(controller.send(:ruact_request?)).to be true
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it "returns true when Accept header includes text/x-component alongside other types" do
|
|
61
|
-
fake_request.headers["Accept"] = "text/x-component, */*"
|
|
62
|
-
expect(controller.send(:ruact_request?)).to be true
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
it "returns true when Ruact-Request: 1 header is set" do
|
|
66
|
-
fake_request.headers["Ruact-Request"] = "1"
|
|
67
|
-
expect(controller.send(:ruact_request?)).to be true
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
it "returns false when Accept: text/html" do
|
|
71
|
-
fake_request.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
72
|
-
expect(controller.send(:ruact_request?)).to be false
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
it "returns false when no Accept header is set" do
|
|
76
|
-
expect(controller.send(:ruact_request?)).to be false
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
describe "#default_render" do
|
|
81
|
-
# Story 10.0 — the activation predicate keys off HTML *acceptability*
|
|
82
|
-
# (`request.accepts`), not `request.format.html?`. `request.format.html?`
|
|
83
|
-
# is false for a `*/*` wildcard, which 500'd those requests pre-10.0.
|
|
84
|
-
before do
|
|
85
|
-
allow(controller).to receive(:ruact_template_exists?).and_return(true)
|
|
86
|
-
allow(controller).to receive(:ruact_render)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "calls ruact_render when the client accepts text/html and template exists (AC#1)" do
|
|
90
|
-
fake_request.accepts = [Mime[:html]]
|
|
91
|
-
controller.send(:default_render)
|
|
92
|
-
expect(controller).to have_received(:ruact_render)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
it "calls ruact_render for a */* wildcard Accept — Mime::ALL (Story 10.0 AC#1)" do
|
|
96
|
-
fake_request.accepts = [Mime::ALL]
|
|
97
|
-
controller.send(:default_render)
|
|
98
|
-
expect(controller).to have_received(:ruact_render)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
it "calls ruact_render when Accept is blank (defaults to HTML) (Story 10.0 AC#1)" do
|
|
102
|
-
fake_request.accepts = []
|
|
103
|
-
controller.send(:default_render)
|
|
104
|
-
expect(controller).to have_received(:ruact_render)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
it "calls ruact_render for RSC requests (text/x-component) even when HTML is unacceptable (AC#1)" do
|
|
108
|
-
fake_request.accepts = [Mime[:json]]
|
|
109
|
-
fake_request.headers["Ruact-Request"] = "1"
|
|
110
|
-
controller.send(:default_render)
|
|
111
|
-
expect(controller).to have_received(:ruact_render)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
it "does NOT call ruact_render for a concrete non-HTML Accept that is not RSC (AC#4 — FR26)" do
|
|
115
|
-
fake_request.accepts = [Mime[:json]]
|
|
116
|
-
begin
|
|
117
|
-
controller.send(:default_render)
|
|
118
|
-
rescue StandardError
|
|
119
|
-
nil
|
|
120
|
-
end
|
|
121
|
-
expect(controller).not_to have_received(:ruact_render)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
it "does NOT call ruact_render when no template exists" do
|
|
125
|
-
allow(controller).to receive(:ruact_template_exists?).and_return(false)
|
|
126
|
-
fake_request.accepts = [Mime[:html]]
|
|
127
|
-
begin
|
|
128
|
-
controller.send(:default_render)
|
|
129
|
-
rescue StandardError
|
|
130
|
-
nil
|
|
131
|
-
end
|
|
132
|
-
expect(controller).not_to have_received(:ruact_render)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
describe "#redirect_to" do
|
|
137
|
-
# Build a class hierarchy so `super` inside the override can call a base implementation.
|
|
138
|
-
# url_for and render must be defined for verify_partial_doubles to allow stubbing them.
|
|
139
|
-
let(:redirect_test_class) do
|
|
140
|
-
base = Class.new do
|
|
141
|
-
def redirect_to(*)
|
|
142
|
-
:super_called
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def url_for(options)
|
|
146
|
-
options.is_a?(String) ? options : "/"
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def render(**_opts); end
|
|
150
|
-
end
|
|
151
|
-
Class.new(base) do
|
|
152
|
-
include Ruact::Controller
|
|
153
|
-
|
|
154
|
-
attr_reader :request
|
|
155
|
-
|
|
156
|
-
def initialize(req)
|
|
157
|
-
super()
|
|
158
|
-
@request = req
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
let(:ruact_ctrl) do
|
|
164
|
-
redirect_test_class.new(Struct.new(:headers, :host).new({ "Accept" => "text/x-component" }, "localhost"))
|
|
165
|
-
end
|
|
166
|
-
let(:html_ctrl) do
|
|
167
|
-
redirect_test_class.new(Struct.new(:headers, :host).new({}, "localhost"))
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
context "when RSC request with same-origin URL (AC #1, #5)" do
|
|
171
|
-
before { allow(ruact_ctrl).to receive(:url_for).and_return("/posts/1") }
|
|
172
|
-
|
|
173
|
-
it "calls render with a Flight redirect row (not a 302)" do
|
|
174
|
-
allow(ruact_ctrl).to receive(:render)
|
|
175
|
-
ruact_ctrl.send(:redirect_to, "/posts/1")
|
|
176
|
-
expect(ruact_ctrl).to have_received(:render).with(
|
|
177
|
-
plain: "0:{\"redirectUrl\":\"/posts/1\",\"redirectType\":\"push\"}\n",
|
|
178
|
-
content_type: "text/x-component"
|
|
179
|
-
)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
it "redirect row matches the flight fixture (AC #5)" do
|
|
183
|
-
rendered_plain = nil
|
|
184
|
-
allow(ruact_ctrl).to receive(:render) { |opts| rendered_plain = opts[:plain] }
|
|
185
|
-
ruact_ctrl.send(:redirect_to, "/posts/1")
|
|
186
|
-
expect(rendered_plain).to match_flight_fixture("redirect_row")
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
context "when RSC request with external URL (AC #3)" do
|
|
191
|
-
before { allow(ruact_ctrl).to receive(:url_for).and_return("https://external.com/page") }
|
|
192
|
-
|
|
193
|
-
it "does NOT emit a redirect row (falls back to super)" do
|
|
194
|
-
allow(ruact_ctrl).to receive(:render)
|
|
195
|
-
ruact_ctrl.send(:redirect_to, "https://external.com/page")
|
|
196
|
-
expect(ruact_ctrl).not_to have_received(:render)
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
context "when non-RSC request (AC #4)" do
|
|
201
|
-
before { allow(html_ctrl).to receive(:url_for).and_return("/posts/1") }
|
|
202
|
-
|
|
203
|
-
it "does NOT emit a redirect row (falls back to super)" do
|
|
204
|
-
allow(html_ctrl).to receive(:render)
|
|
205
|
-
html_ctrl.send(:redirect_to, "/posts/1")
|
|
206
|
-
expect(html_ctrl).not_to have_received(:render)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
describe "#ruact_html_shell" do
|
|
212
|
-
# Story 14.2 — the entry `<script>` tags are emitted by the mixed-in
|
|
213
|
-
# ViewHelper#ruact_vite_tags (which needs Rails.env / a live Vite probe).
|
|
214
|
-
# Stub it to "" so we test the shell structure + the __FLIGHT_DATA script
|
|
215
|
-
# (still emitted by ruact_js_assets) in isolation.
|
|
216
|
-
before { allow(controller).to receive(:ruact_vite_tags).and_return("") }
|
|
217
|
-
|
|
218
|
-
let(:payload) { "0:[\"$\",\"div\",null,{}]\n" }
|
|
219
|
-
|
|
220
|
-
it "returns a string containing window.__FLIGHT_DATA" do
|
|
221
|
-
html = controller.send(:ruact_html_shell, payload)
|
|
222
|
-
expect(html).to include("__FLIGHT_DATA")
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
it "wraps the payload in an IIFE push" do
|
|
226
|
-
html = controller.send(:ruact_html_shell, payload)
|
|
227
|
-
expect(html).to include("d.push(")
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
it "contains a root div#root element" do
|
|
231
|
-
html = controller.send(:ruact_html_shell, payload)
|
|
232
|
-
expect(html).to include('<div id="root">')
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
it "escapes </script> in the payload to prevent XSS breakout" do
|
|
236
|
-
dangerous_payload = "0:\"</script><script>alert(1)</script>\"\n"
|
|
237
|
-
html = controller.send(:ruact_html_shell, dangerous_payload)
|
|
238
|
-
# The HTML must contain exactly ONE </script> — the real closing tag of the script block.
|
|
239
|
-
# If the payload's </script> leaked through, there would be more than one.
|
|
240
|
-
occurrences = html.scan("</script>")
|
|
241
|
-
count = occurrences.length
|
|
242
|
-
expect(count).to eq(1), "Expected 1 </script> (closing tag), found #{count}"
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Story 8.3 review R7 — the shell must surface the host's CSRF token
|
|
246
|
-
# as a `<meta name="csrf-token">` tag so the JS runtime can forward
|
|
247
|
-
# it as `X-CSRF-Token` on every `_makeRef` call. Without this,
|
|
248
|
-
# standalone server actions (Story 8.3 AC5) — for which the gem
|
|
249
|
-
# enforces CSRF itself via `protect_from_forgery with: :exception,
|
|
250
|
-
# if: :dispatching_standalone?` — can never authenticate, because
|
|
251
|
-
# the document has no token for the runtime to read.
|
|
252
|
-
context "Story 8.3 — CSRF meta tag injection", :story_8_3 do
|
|
253
|
-
# The bare `test_class` above is a minimal `include Ruact::Controller`
|
|
254
|
-
# consumer with no `form_authenticity_token` surface (no Rails
|
|
255
|
-
# request-forgery-protection module mixed in). Define a real method
|
|
256
|
-
# on the class so `respond_to?(:form_authenticity_token, true)` is
|
|
257
|
-
# true AND `verify_partial_doubles` lets us stub the return value.
|
|
258
|
-
let(:csrf_test_class) do
|
|
259
|
-
Class.new(test_class) do
|
|
260
|
-
def form_authenticity_token
|
|
261
|
-
"stub-default-token"
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
let(:csrf_controller) { csrf_test_class.new(fake_request) }
|
|
266
|
-
|
|
267
|
-
# Story 14.2 — csrf_controller is a separate instance from `controller`,
|
|
268
|
-
# so the outer `before` stub does not reach it. Stub its entry-tag
|
|
269
|
-
# emission too (the real path would hit Rails.root in a non-booted env),
|
|
270
|
-
# keeping these CSRF tests order-independent.
|
|
271
|
-
before { allow(csrf_controller).to receive(:ruact_vite_tags).and_return("") }
|
|
272
|
-
|
|
273
|
-
it "embeds <meta name=\"csrf-token\" content=\"...\"> when the host exposes form_authenticity_token" do
|
|
274
|
-
allow(csrf_controller).to receive(:form_authenticity_token).and_return("test-csrf-token-value")
|
|
275
|
-
html = csrf_controller.send(:ruact_html_shell, payload)
|
|
276
|
-
expect(html).to include('<meta name="csrf-token" content="test-csrf-token-value" />')
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
it "HTML-escapes the token value (defense against accidental quote injection)" do
|
|
280
|
-
allow(csrf_controller).to receive(:form_authenticity_token).and_return('"quoted" & <evil>')
|
|
281
|
-
html = csrf_controller.send(:ruact_html_shell, payload)
|
|
282
|
-
expect(html).to include(""quoted" & <evil>")
|
|
283
|
-
expect(html).not_to include('"quoted" & <evil>')
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
it "omits the meta tag (renders empty string) when form_authenticity_token is not available " \
|
|
287
|
-
"(e.g., a non-Rails spec context)" do
|
|
288
|
-
# `controller` is the bare test_class without `form_authenticity_token`.
|
|
289
|
-
html = controller.send(:ruact_html_shell, payload)
|
|
290
|
-
expect(html).not_to include('name="csrf-token"')
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
it "omits the meta tag when form_authenticity_token returns nil/empty" do
|
|
294
|
-
allow(csrf_controller).to receive(:form_authenticity_token).and_return("")
|
|
295
|
-
html = csrf_controller.send(:ruact_html_shell, payload)
|
|
296
|
-
expect(html).not_to include('name="csrf-token"')
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
it "silently omits the meta tag if form_authenticity_token raises " \
|
|
300
|
-
"(e.g., session middleware missing in a stripped-down test env)" do
|
|
301
|
-
allow(csrf_controller).to receive(:form_authenticity_token).and_raise(StandardError, "no session")
|
|
302
|
-
expect { csrf_controller.send(:ruact_html_shell, payload) }.not_to raise_error
|
|
303
|
-
html = csrf_controller.send(:ruact_html_shell, payload)
|
|
304
|
-
expect(html).not_to include('name="csrf-token"')
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
# Story 14.2 (FR104) — the controller's HTML shell delegates its JS asset
|
|
310
|
-
# markup (entry tags + __FLIGHT_DATA) to the single
|
|
311
|
-
# Ruact::ViewHelper#ruact_js_assets implementation. No duplicated tag logic;
|
|
312
|
-
# controller output == a view helper's output (parity guards against drift).
|
|
313
|
-
describe "ruact_js_assets delegation + parity", :story_14_2 do
|
|
314
|
-
let(:payload) { "0:[\"$\",\"div\",null,{}]\n" }
|
|
315
|
-
|
|
316
|
-
it "delegates the JS block to ruact_js_assets (single implementation)" do
|
|
317
|
-
allow(controller).to receive(:ruact_js_assets).with(payload).and_return("<!--RUACT-JS-->".html_safe)
|
|
318
|
-
html = controller.send(:ruact_html_shell, payload)
|
|
319
|
-
expect(html).to include("<!--RUACT-JS-->")
|
|
320
|
-
expect(controller).to have_received(:ruact_js_assets).with(payload)
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
it "emits markup byte-identical to a standalone view helper (no drift)" do
|
|
324
|
-
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
|
|
325
|
-
manifest_entry = { "file" => "bootstrap-xyz789.js" }
|
|
326
|
-
allow(controller).to receive(:vite_manifest_entry).and_return(manifest_entry)
|
|
327
|
-
|
|
328
|
-
view = Object.new
|
|
329
|
-
view.extend(Ruact::ViewHelper)
|
|
330
|
-
allow(view).to receive(:vite_manifest_entry).and_return(manifest_entry)
|
|
331
|
-
|
|
332
|
-
expect(controller.send(:ruact_js_assets, payload)).to eq(view.ruact_js_assets(payload))
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
it "exposes ruact_js_assets as PRIVATE on the controller (never a routable action)", :aggregate_failures do
|
|
336
|
-
expect(controller.class.public_method_defined?(:ruact_js_assets)).to be false
|
|
337
|
-
expect(controller.class.private_method_defined?(:ruact_js_assets)).to be true
|
|
338
|
-
# __ruact_component__ is likewise demoted so it is not exposed as an action.
|
|
339
|
-
expect(controller.class.public_method_defined?(:__ruact_component__)).to be false
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
end
|