ruact 0.0.11 → 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 +110 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +14 -4
- 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 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- 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,785 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Story 9.2 — request-cycle spec for dual-bucket response negotiation on the
|
|
4
|
-
# SAME controller action, on the `Ruact::Server` concern. Pins, against REAL
|
|
5
|
-
# host-controller routes:
|
|
6
|
-
#
|
|
7
|
-
# - Bucket 2 (Accept: application/json, non-GET) normal completion → JSON of
|
|
8
|
-
# all exposed ivars keyed by name, via ruact_props/Serializable (AC2)
|
|
9
|
-
# - Bucket 2 redirect_to → { "$redirect": "<path>" } (AC3)
|
|
10
|
-
# - Bucket 2 empty → 204, no body (AC4)
|
|
11
|
-
# - Bucket 2 serialization failure → 9.1 chain → 500 structured (AC5)
|
|
12
|
-
# - Vary: Accept on EVERY non-GET shape — 200 / 204 / $redirect / Flight (AC6)
|
|
13
|
-
# - CSRF on Bucket 2 — host protect_from_forgery (AC7)
|
|
14
|
-
# - Bucket 1 (text/x-component) redirect → Flight redirect row, unchanged (AC1)
|
|
15
|
-
#
|
|
16
|
-
# Mounts on the shared Story-7.9 Rails app; mirrors the 9.1 request-spec pattern.
|
|
17
|
-
|
|
18
|
-
require "action_controller/railtie"
|
|
19
|
-
require "action_view/railtie"
|
|
20
|
-
|
|
21
|
-
require "spec_helper"
|
|
22
|
-
require "rack/test"
|
|
23
|
-
|
|
24
|
-
require "ruact/controller"
|
|
25
|
-
require "ruact/server"
|
|
26
|
-
require "active_model"
|
|
27
|
-
|
|
28
|
-
require_relative "controller_request_spec" unless defined?(ControllerRequestSpecSupport)
|
|
29
|
-
|
|
30
|
-
module ServerBucketSpecSupport
|
|
31
|
-
# Story 13.3 (FR98) — an ActiveModel record with a presence validation, so a
|
|
32
|
-
# host action can exercise the non-exception `if record.valid? … else …`
|
|
33
|
-
# happy-failure path that `ruact_errors` round-trips.
|
|
34
|
-
class ValidatedPost
|
|
35
|
-
include ActiveModel::Model
|
|
36
|
-
include ActiveModel::Attributes
|
|
37
|
-
|
|
38
|
-
attribute :title, :string
|
|
39
|
-
|
|
40
|
-
validates :title, presence: true
|
|
41
|
-
|
|
42
|
-
def self.name = "ValidatedPost"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Serializable model — only :id/:title exposed; :secret must never leak.
|
|
46
|
-
class BucketPost
|
|
47
|
-
include Ruact::Serializable
|
|
48
|
-
|
|
49
|
-
attr_reader :id, :title, :secret
|
|
50
|
-
|
|
51
|
-
def initialize(id:, title:, secret:)
|
|
52
|
-
@id = id
|
|
53
|
-
@title = title
|
|
54
|
-
@secret = secret
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
ruact_props :id, :title
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Non-Serializable record with as_json — under strict_serialization it must
|
|
61
|
-
# raise Ruact::SerializationError (AC5).
|
|
62
|
-
class UnserializableRecord
|
|
63
|
-
def as_json(_opts = nil)
|
|
64
|
-
{ "id" => 1, "leak" => "everything" }
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Bucket-2 host: includes ONLY Ruact::Server (Bucket-2 requests are handled
|
|
69
|
-
# by Server#default_render without delegating to Ruact::Controller).
|
|
70
|
-
class BucketServerController < ActionController::Base
|
|
71
|
-
include Ruact::Server
|
|
72
|
-
|
|
73
|
-
def with_ivars
|
|
74
|
-
@post = BucketPost.new(id: 1, title: "Hi", secret: "topsecret")
|
|
75
|
-
@count = 2
|
|
76
|
-
# no explicit render → default_render
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def empty_action
|
|
80
|
-
# sets no exposed ivars, no redirect → 204 on Bucket 2
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def redirecting
|
|
84
|
-
redirect_to "/posts/1"
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Review round 1 — cross-host redirect must hit Rails' open-redirect
|
|
88
|
-
# protection (raise) instead of silently emitting an external $redirect.
|
|
89
|
-
def redirect_external
|
|
90
|
-
redirect_to "https://evil.example.com/x"
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def redirect_external_allowed
|
|
94
|
-
redirect_to "https://allowed.example.com/x", allow_other_host: true
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Review round 1 — a host that sets Vary itself must keep it; Accept is
|
|
98
|
-
# appended, not clobbered.
|
|
99
|
-
def vary_clobber
|
|
100
|
-
response.headers["Vary"] = "Cookie"
|
|
101
|
-
@ok = true
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Review round 2 — a host that sets Vary: * (uncacheable wildcard) must keep
|
|
105
|
-
# it as-is, not "*, Accept".
|
|
106
|
-
def vary_wildcard
|
|
107
|
-
response.headers["Vary"] = "*"
|
|
108
|
-
@ok = true
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def unserializable
|
|
112
|
-
@rec = UnserializableRecord.new
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Story 13.3 (FR98) — opted-in FAILED validation: `ruact_errors(post)` after
|
|
116
|
-
# an invalid save attempt registers the canonical errors; Bucket-2 injects
|
|
117
|
-
# them under the reserved `errors` key alongside the serialized ivars.
|
|
118
|
-
def validated_create_invalid
|
|
119
|
-
post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
|
|
120
|
-
@created = post.valid? # false → populates post.errors
|
|
121
|
-
ruact_errors(post)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Story 13.3 (FR98) — opted-in SUCCESS: the SAME call path yields
|
|
125
|
-
# `errors: {}` (success symmetry), so the client reads `result.errors`
|
|
126
|
-
# uniformly on both branches.
|
|
127
|
-
def validated_create_valid
|
|
128
|
-
post = ServerBucketSpecSupport::ValidatedPost.new(title: "Hi")
|
|
129
|
-
@created = post.valid? # true → post.errors empty
|
|
130
|
-
ruact_errors(post)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Story 13.3 (FR98) — a stray dev ivar literally named `@errors` must NOT
|
|
134
|
-
# clobber the reserved round-trip key: the collector is the source of truth.
|
|
135
|
-
def stray_errors_ivar
|
|
136
|
-
@errors = "a dev string that must not win"
|
|
137
|
-
post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
|
|
138
|
-
post.valid?
|
|
139
|
-
ruact_errors(post)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Story 15.0 (F6) — the injection-BYPASS case the dev warning targets:
|
|
143
|
-
# registers errors, then renders explicitly on the same branch, so
|
|
144
|
-
# `default_render` never runs and the errors vanish from the JSON body.
|
|
145
|
-
def opt_out_render
|
|
146
|
-
post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
|
|
147
|
-
post.valid?
|
|
148
|
-
ruact_errors(post)
|
|
149
|
-
render json: { "ok" => false }
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Story 15.0 (F6 guardrail a) — the documented-correct Bucket-1 pattern:
|
|
153
|
-
# register errors, render the form page again (the template binds the
|
|
154
|
-
# `ruact_errors` reader). Requested WITHOUT the function-call Accept.
|
|
155
|
-
def failed_with_page_render
|
|
156
|
-
post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
|
|
157
|
-
post.valid?
|
|
158
|
-
ruact_errors(post)
|
|
159
|
-
render :new_form
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Story 15.0 (F6 guardrail b) — register errors, then redirect (the
|
|
163
|
-
# redirect-back round-trip). Correct on BOTH buckets, must never warn.
|
|
164
|
-
def failed_with_redirect
|
|
165
|
-
post = ServerBucketSpecSupport::ValidatedPost.new(title: nil)
|
|
166
|
-
post.valid?
|
|
167
|
-
ruact_errors(post)
|
|
168
|
-
redirect_to "/posts/new"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Story 15.0 (F6 guardrail d) — explicit render with an UNTOUCHED collector.
|
|
172
|
-
def untouched_explicit_render
|
|
173
|
-
render json: { "ok" => true }
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Story 15.5 (FR109) — a plain Rails action on a `Ruact::Server` controller:
|
|
177
|
-
# a GET that renders plain text, so it is neither a function call nor a ruact
|
|
178
|
-
# page render. The dev shape-log must stay SILENT on it (non-negotiated).
|
|
179
|
-
def plain_rails
|
|
180
|
-
render plain: "just rails"
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Review round 2 — an auth-style before_action that redirects on a Bucket-2
|
|
185
|
-
# call: the response is performed in the before_action, so after-callbacks are
|
|
186
|
-
# skipped — Vary must still be present (set by a before_action too).
|
|
187
|
-
class BucketBeforeRedirectController < ActionController::Base
|
|
188
|
-
include Ruact::Server
|
|
189
|
-
|
|
190
|
-
before_action :bounce
|
|
191
|
-
|
|
192
|
-
def never_runs
|
|
193
|
-
@ok = true
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
private
|
|
197
|
-
|
|
198
|
-
def bounce
|
|
199
|
-
redirect_to "/login"
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Bucket-1 regression host: BOTH concerns, so a non-function-call request
|
|
204
|
-
# delegates Server#redirect_to → super → Ruact::Controller's Flight row.
|
|
205
|
-
class BucketDualController < ActionController::Base
|
|
206
|
-
include Ruact::Controller
|
|
207
|
-
include Ruact::Server
|
|
208
|
-
|
|
209
|
-
def redirecting
|
|
210
|
-
redirect_to "/posts/1"
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# CSRF-enforcing Bucket-2 host (AC7) — forgery flipped on per-example.
|
|
215
|
-
class BucketForgeryController < ActionController::Base
|
|
216
|
-
include Ruact::Server
|
|
217
|
-
|
|
218
|
-
protect_from_forgery with: :exception
|
|
219
|
-
|
|
220
|
-
def create_protected
|
|
221
|
-
@ok = true
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# GET is exempt from CSRF verification — emits a per-request token masked
|
|
225
|
-
# against the session master, for the valid-token round-trip (AC7).
|
|
226
|
-
def csrf_token
|
|
227
|
-
render json: { token: form_authenticity_token }
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Story 10.0 (AC5) — the scaffold's resource-controller shape: includes BOTH
|
|
232
|
-
# Ruact::Controller and Ruact::Server (ApplicationController contributes
|
|
233
|
-
# Controller; the resource controller adds Server). An ivar-only GET page
|
|
234
|
-
# action (implicit `default_render`) backed by a conventional template must
|
|
235
|
-
# flow Server#default_render → super (GET is never a function call) → the
|
|
236
|
-
# now-fixed Controller#default_render → HTML shell, NOT a 500.
|
|
237
|
-
class ServerImplicitController < ActionController::Base
|
|
238
|
-
include Ruact::Controller
|
|
239
|
-
include Ruact::Server
|
|
240
|
-
|
|
241
|
-
def show; end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Story 10.0 (AC5) — conventional template under the shared app root so the
|
|
246
|
-
# Server-including controller's implicit GET reaches Controller#default_render.
|
|
247
|
-
ControllerRequestSpecSupport.write_view(
|
|
248
|
-
"server_bucket_spec_support/server_implicit", "show", <<~ERB
|
|
249
|
-
<div>
|
|
250
|
-
<DemoButton label={"hi"} />
|
|
251
|
-
</div>
|
|
252
|
-
ERB
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
# Story 15.0 (F6 guardrail a) — the re-rendered form page for the documented
|
|
256
|
-
# Bucket-1 `ruact_errors(record); render :new_form` pattern. No component tags
|
|
257
|
-
# on purpose (the guardrail is about the warn predicate, not the pipeline).
|
|
258
|
-
ControllerRequestSpecSupport.write_view(
|
|
259
|
-
"server_bucket_spec_support/bucket_server", "new_form", <<~ERB
|
|
260
|
-
<div>the form again</div>
|
|
261
|
-
ERB
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
if defined?(ControllerRequestSpecSupport) &&
|
|
265
|
-
!ControllerRequestSpecSupport.instance_variable_get(:@__ruact_server_bucket_routes_appended)
|
|
266
|
-
ControllerRequestSpecSupport.instance_variable_set(:@__ruact_server_bucket_routes_appended, true)
|
|
267
|
-
ControllerRequestSpecSupport.app_class.routes.append do
|
|
268
|
-
post "/bucket/with_ivars", to: "server_bucket_spec_support/bucket_server#with_ivars"
|
|
269
|
-
post "/bucket/empty", to: "server_bucket_spec_support/bucket_server#empty_action"
|
|
270
|
-
post "/bucket/redirecting", to: "server_bucket_spec_support/bucket_server#redirecting"
|
|
271
|
-
post "/bucket/redirect_external", to: "server_bucket_spec_support/bucket_server#redirect_external"
|
|
272
|
-
post "/bucket/redirect_external_allowed", to: "server_bucket_spec_support/bucket_server#redirect_external_allowed"
|
|
273
|
-
post "/bucket/vary_clobber", to: "server_bucket_spec_support/bucket_server#vary_clobber"
|
|
274
|
-
post "/bucket/vary_wildcard", to: "server_bucket_spec_support/bucket_server#vary_wildcard"
|
|
275
|
-
post "/bucket/before_redirect", to: "server_bucket_spec_support/bucket_before_redirect#never_runs"
|
|
276
|
-
post "/bucket/unserializable", to: "server_bucket_spec_support/bucket_server#unserializable"
|
|
277
|
-
post "/bucket/validated_create_invalid", to: "server_bucket_spec_support/bucket_server#validated_create_invalid"
|
|
278
|
-
post "/bucket/validated_create_valid", to: "server_bucket_spec_support/bucket_server#validated_create_valid"
|
|
279
|
-
post "/bucket/stray_errors_ivar", to: "server_bucket_spec_support/bucket_server#stray_errors_ivar"
|
|
280
|
-
# Story 15.0 (F6) — the warning truth-table routes.
|
|
281
|
-
post "/bucket/opt_out_render", to: "server_bucket_spec_support/bucket_server#opt_out_render"
|
|
282
|
-
post "/bucket/failed_with_page_render",
|
|
283
|
-
to: "server_bucket_spec_support/bucket_server#failed_with_page_render"
|
|
284
|
-
post "/bucket/failed_with_redirect", to: "server_bucket_spec_support/bucket_server#failed_with_redirect"
|
|
285
|
-
post "/bucket/untouched_explicit_render",
|
|
286
|
-
to: "server_bucket_spec_support/bucket_server#untouched_explicit_render"
|
|
287
|
-
# Story 15.5 (FR109) — a plain Rails GET on a Server controller (non-negotiated).
|
|
288
|
-
get "/bucket/plain_rails", to: "server_bucket_spec_support/bucket_server#plain_rails"
|
|
289
|
-
post "/bucket/dual_redirecting", to: "server_bucket_spec_support/bucket_dual#redirecting"
|
|
290
|
-
post "/bucket/protected", to: "server_bucket_spec_support/bucket_forgery#create_protected"
|
|
291
|
-
get "/bucket/csrf_token", to: "server_bucket_spec_support/bucket_forgery#csrf_token"
|
|
292
|
-
# Story 10.0 (AC5) — Server-including controller's implicit GET page action.
|
|
293
|
-
get "/server-implicit/show", to: "server_bucket_spec_support/server_implicit#show"
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
RSpec.describe "Story 9.2: Ruact::Server dual-bucket response negotiation", :story_9_2 do
|
|
298
|
-
include Rack::Test::Methods
|
|
299
|
-
|
|
300
|
-
let(:app_class) { ControllerRequestSpecSupport.app_class }
|
|
301
|
-
let(:app) { app_class.instance }
|
|
302
|
-
|
|
303
|
-
let(:json_headers) { { "CONTENT_TYPE" => "application/json", "HTTP_ACCEPT" => "application/json" } }
|
|
304
|
-
let(:flight_headers) { { "HTTP_ACCEPT" => "text/x-component" } }
|
|
305
|
-
|
|
306
|
-
before do
|
|
307
|
-
Rails.logger = Logger.new(IO::NULL)
|
|
308
|
-
ControllerRequestSpecSupport.boot!
|
|
309
|
-
# Story 10.0 (AC5) — pin Rails.root so the Server-path implicit-render
|
|
310
|
-
# template (`Rails.root/app/views`) is found regardless of full-suite
|
|
311
|
-
# ordering leftovers (see controller_request_spec.rb for the rationale).
|
|
312
|
-
Rails.root = ControllerRequestSpecSupport.app_root
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def reset_config
|
|
316
|
-
Ruact.instance_variable_set(:@config, nil)
|
|
317
|
-
Ruact.instance_variable_set(:@configured_at_least_once, false)
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
describe "Bucket 2 — normal completion serializes all exposed ivars (AC2)" do
|
|
321
|
-
it "renders 200 + a JSON object keyed by ivar name, ruact_props only (no secret)" do
|
|
322
|
-
post "/bucket/with_ivars", "{}", json_headers
|
|
323
|
-
expect(last_response.status).to eq(200)
|
|
324
|
-
expect(last_response.headers["Content-Type"]).to include("application/json")
|
|
325
|
-
body = JSON.parse(last_response.body)
|
|
326
|
-
expect(body).to eq("post" => { "id" => 1, "title" => "Hi" }, "count" => 2)
|
|
327
|
-
expect(body.fetch("post")).not_to have_key("secret")
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
describe "Bucket 2 — redirect_to surfaces as $redirect (AC3)" do
|
|
332
|
-
it "renders 200 + { \"$redirect\": \"<path>\" } instead of a 302 / Flight row" do
|
|
333
|
-
post "/bucket/redirecting", "{}", json_headers
|
|
334
|
-
expect(last_response.status).to eq(200)
|
|
335
|
-
expect(JSON.parse(last_response.body)).to eq("$redirect" => "/posts/1")
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
describe "Bucket 2 — redirect open-redirect protection (review round 1)" do
|
|
340
|
-
# Turn Rails' open-redirect protection ON for this app (the bare test app
|
|
341
|
-
# doesn't `load_defaults`, so it defaults to log-and-allow). `raise_on_open_redirects`
|
|
342
|
-
# forces the raise path on both Rails 7.0 and 8.x.
|
|
343
|
-
around do |example|
|
|
344
|
-
previous = ActionController::Base.raise_on_open_redirects
|
|
345
|
-
ActionController::Base.raise_on_open_redirects = true
|
|
346
|
-
example.run
|
|
347
|
-
ensure
|
|
348
|
-
ActionController::Base.raise_on_open_redirects = previous
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
it "a cross-host redirect_to raises (open-redirect protection) → 500 structured, matching Rails" do
|
|
352
|
-
post "/bucket/redirect_external", "{}", json_headers
|
|
353
|
-
expect(last_response.status).to eq(500)
|
|
354
|
-
expect(JSON.parse(last_response.body).fetch("error_class")).to match(/RedirectError/)
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
it "honors allow_other_host: true — emits the absolute $redirect" do
|
|
358
|
-
post "/bucket/redirect_external_allowed", "{}", json_headers
|
|
359
|
-
expect(last_response.status).to eq(200)
|
|
360
|
-
expect(JSON.parse(last_response.body)).to eq("$redirect" => "https://allowed.example.com/x")
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
describe "Vary preserves a host-set value (review round 1)" do
|
|
365
|
-
it "appends Accept to a Vary the action set, never clobbering it" do
|
|
366
|
-
post "/bucket/vary_clobber", "{}", json_headers
|
|
367
|
-
vary = last_response.headers["Vary"]
|
|
368
|
-
expect(vary).to match(/\bCookie\b/)
|
|
369
|
-
expect(vary).to match(/\bAccept\b/)
|
|
370
|
-
end
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
describe "Vary edge cases (review round 2)" do
|
|
374
|
-
it "is set on a $redirect performed by a before_action (after-callbacks skipped)" do
|
|
375
|
-
post "/bucket/before_redirect", "{}", json_headers
|
|
376
|
-
expect(JSON.parse(last_response.body)).to eq("$redirect" => "/login")
|
|
377
|
-
expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
it "preserves a host Vary: * wildcard as-is (does not produce '*, Accept')" do
|
|
381
|
-
post "/bucket/vary_wildcard", "{}", json_headers
|
|
382
|
-
expect(last_response.headers["Vary"]).to eq("*")
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
describe "Bucket 2 — empty action → 204 (AC4)" do
|
|
387
|
-
it "returns 204 No Content with an empty body" do
|
|
388
|
-
post "/bucket/empty", "{}", json_headers
|
|
389
|
-
expect(last_response.status).to eq(204)
|
|
390
|
-
expect(last_response.body).to eq("")
|
|
391
|
-
end
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
describe "Bucket 2 — serialization failure routes through the 9.1 chain as 500 (AC5/AC9)" do
|
|
395
|
-
before do
|
|
396
|
-
# Re-prime AFTER spec_helper's global before resets @config, so strict
|
|
397
|
-
# sticks for the example body (local before runs after global before).
|
|
398
|
-
reset_config
|
|
399
|
-
Ruact.configure { |c| c.strict_serialization = true }
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
it "raises Ruact::SerializationError → 500 structured payload", :aggregate_failures do
|
|
403
|
-
post "/bucket/unserializable", "{}", json_headers
|
|
404
|
-
expect(last_response.status).to eq(500)
|
|
405
|
-
body = JSON.parse(last_response.body)
|
|
406
|
-
expect(body.fetch("_ruact_server_action_error")).to be(true)
|
|
407
|
-
expect(body.fetch("error_class")).to eq("Ruact::SerializationError")
|
|
408
|
-
expect(body.fetch("message")).to match(/Cannot serialize/)
|
|
409
|
-
end
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
describe "Vary: Accept on every non-GET shape (AC6)" do
|
|
413
|
-
it "is set on the 200 ivar-JSON response" do
|
|
414
|
-
post "/bucket/with_ivars", "{}", json_headers
|
|
415
|
-
expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
it "is set on the 204 response" do
|
|
419
|
-
post "/bucket/empty", "{}", json_headers
|
|
420
|
-
expect(last_response.status).to eq(204)
|
|
421
|
-
expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
it "is set on the $redirect response" do
|
|
425
|
-
post "/bucket/redirecting", "{}", json_headers
|
|
426
|
-
expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
it "is set on the Bucket-1 Flight response too" do
|
|
430
|
-
post "/bucket/dual_redirecting", "{}", flight_headers
|
|
431
|
-
expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
describe "Bucket 1 — form/navigation Flight path unchanged (AC1)" do
|
|
436
|
-
it "a text/x-component redirect still emits the Flight redirect row (no $redirect JSON)" do
|
|
437
|
-
post "/bucket/dual_redirecting", "{}", flight_headers
|
|
438
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
439
|
-
expect(last_response.body).to eq("0:#{JSON.generate({ 'redirectUrl' => '/posts/1',
|
|
440
|
-
'redirectType' => 'push' })}\n")
|
|
441
|
-
end
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
describe "CSRF on Bucket 2 — host protect_from_forgery (AC7)" do
|
|
445
|
-
around do |example|
|
|
446
|
-
previous = ServerBucketSpecSupport::BucketForgeryController.allow_forgery_protection
|
|
447
|
-
ServerBucketSpecSupport::BucketForgeryController.allow_forgery_protection = true
|
|
448
|
-
example.run
|
|
449
|
-
ensure
|
|
450
|
-
ServerBucketSpecSupport::BucketForgeryController.allow_forgery_protection = previous
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
it "valid X-CSRF-Token → 200 + serialized ivars (full round-trip)" do
|
|
454
|
-
get "/bucket/csrf_token"
|
|
455
|
-
token = JSON.parse(last_response.body).fetch("token")
|
|
456
|
-
post "/bucket/protected", "{}", json_headers.merge("HTTP_X_CSRF_TOKEN" => token)
|
|
457
|
-
expect(last_response.status).to eq(200)
|
|
458
|
-
expect(JSON.parse(last_response.body)).to eq("ok" => true)
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
it "missing X-CSRF-Token → 403 structured (via the 9.1 chain)" do
|
|
462
|
-
post "/bucket/protected", "{}", json_headers
|
|
463
|
-
expect(last_response.status).to eq(403)
|
|
464
|
-
expect(JSON.parse(last_response.body).fetch("error_class")).to eq("ActionController::InvalidAuthenticityToken")
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
it "invalid X-CSRF-Token → 403 structured" do
|
|
468
|
-
post "/bucket/protected", "{}", json_headers.merge("HTTP_X_CSRF_TOKEN" => "nope-not-valid")
|
|
469
|
-
expect(last_response.status).to eq(403)
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
context "with forgery protection OFF (API mode)" do
|
|
473
|
-
around do |example|
|
|
474
|
-
ServerBucketSpecSupport::BucketForgeryController.allow_forgery_protection = false
|
|
475
|
-
example.run
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
it "accepts a tokenless request and serializes ivars" do
|
|
479
|
-
post "/bucket/protected", "{}", json_headers
|
|
480
|
-
expect(last_response.status).to eq(200)
|
|
481
|
-
expect(JSON.parse(last_response.body)).to eq("ok" => true)
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
# Story 13.3 (FR98) — the Bucket-2 (imperative `await`) half of the
|
|
487
|
-
# Inertia-style validation `errors` round-trip (AC3).
|
|
488
|
-
describe "Story 13.3: Bucket-2 errors round-trip (FR98)", :story_13_3 do
|
|
489
|
-
it "FAILED validation injects the canonical errors under the reserved `errors` key" do
|
|
490
|
-
post "/bucket/validated_create_invalid", "{}", json_headers
|
|
491
|
-
expect(last_response.status).to eq(200)
|
|
492
|
-
body = JSON.parse(last_response.body)
|
|
493
|
-
expect(body).to eq("created" => false, "errors" => { "title" => ["Title can't be blank"] })
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
it "SUCCESS yields `errors: {}` through the same call path (success symmetry)" do
|
|
497
|
-
post "/bucket/validated_create_valid", "{}", json_headers
|
|
498
|
-
expect(last_response.status).to eq(200)
|
|
499
|
-
body = JSON.parse(last_response.body)
|
|
500
|
-
expect(body).to eq("created" => true, "errors" => {})
|
|
501
|
-
end
|
|
502
|
-
|
|
503
|
-
it "is set with Vary: Accept like every other non-GET shape" do
|
|
504
|
-
post "/bucket/validated_create_valid", "{}", json_headers
|
|
505
|
-
expect(last_response.headers["Vary"]).to match(/\bAccept\b/)
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
it "the collector wins over a stray dev ivar literally named @errors" do
|
|
509
|
-
post "/bucket/stray_errors_ivar", "{}", json_headers
|
|
510
|
-
expect(last_response.status).to eq(200)
|
|
511
|
-
body = JSON.parse(last_response.body)
|
|
512
|
-
expect(body.fetch("errors")).to eq("title" => ["Title can't be blank"])
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
it "preserves the 9.2 untouched-collector contract: an empty opted-out action still 204s" do
|
|
516
|
-
# `empty_action` never calls `ruact_errors` → no `errors` key, 204 No Content.
|
|
517
|
-
post "/bucket/empty", "{}", json_headers
|
|
518
|
-
expect(last_response.status).to eq(204)
|
|
519
|
-
expect(last_response.body).to eq("")
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
it "preserves the 9.2 untouched-collector contract: a populated action carries NO errors key" do
|
|
523
|
-
# `with_ivars` sets ivars but never opts in → body has no `errors` key.
|
|
524
|
-
post "/bucket/with_ivars", "{}", json_headers
|
|
525
|
-
expect(JSON.parse(last_response.body)).not_to have_key("errors")
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
# AC5(e) coexistence: the RAISED `ActiveRecord::RecordInvalid` path still
|
|
529
|
-
# renders the structured 422 error payload — proven (unchanged) by
|
|
530
|
-
# server_rescue_request_spec.rb's `record_invalid` example. 13.3 is the
|
|
531
|
-
# NON-raised 200 path above; the two contracts stay distinct.
|
|
532
|
-
end
|
|
533
|
-
|
|
534
|
-
# Story 15.0 (F6) — the dev-mode injection opt-out warning truth table.
|
|
535
|
-
# Fires ONLY on: development + function-call request + touched collector +
|
|
536
|
-
# explicit host render. Silent on every documented-correct pattern.
|
|
537
|
-
describe "Story 15.0: dev-mode ruact_errors injection opt-out warning (F6)", :story_15_0 do
|
|
538
|
-
# The distinctive fragment of the F6 message (deliberately distinct from
|
|
539
|
-
# any other `[ruact]` log line, incl. the future 15.5 bucket log).
|
|
540
|
-
let(:warning_re) { /\[ruact\] .*called `ruact_errors` and then rendered explicitly/ }
|
|
541
|
-
let(:html_form_headers) do
|
|
542
|
-
{ "CONTENT_TYPE" => "application/x-www-form-urlencoded", "HTTP_ACCEPT" => "text/html" }
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
before do
|
|
546
|
-
allow(Rails.logger).to receive(:warn)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
# NOTE: the suite's default Rails.env is "development" (RAILS_ENV unset),
|
|
550
|
-
# so the dev examples stub it EXPLICITLY anyway (they must not depend on
|
|
551
|
-
# the suite default) and the non-dev example stubs a non-dev env.
|
|
552
|
-
def stub_env(name)
|
|
553
|
-
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new(name))
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
def stub_development_env
|
|
557
|
-
stub_env("development")
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
it "warns exactly once on an explicit render after ruact_errors on a function-call request" do
|
|
561
|
-
stub_development_env
|
|
562
|
-
post "/bucket/opt_out_render", "{}", json_headers
|
|
563
|
-
|
|
564
|
-
expect(Rails.logger).to have_received(:warn).with(
|
|
565
|
-
a_string_matching(/\[ruact\] .*BucketServerController#opt_out_render.*ruact_errors.*NOT.*injected/m)
|
|
566
|
-
).once
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
it "names the fix in the message (fall through, page-render binding, or redirect)" do
|
|
570
|
-
stub_development_env
|
|
571
|
-
post "/bucket/opt_out_render", "{}", json_headers
|
|
572
|
-
|
|
573
|
-
expect(Rails.logger).to have_received(:warn).with(
|
|
574
|
-
a_string_matching(/fall.*through.*errors=\{ruact_errors\}.*redirect_to/m)
|
|
575
|
-
)
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
it "keeps the response byte-identical whether the warning fires or not (log-only)" do
|
|
579
|
-
stub_env("production")
|
|
580
|
-
post "/bucket/opt_out_render", "{}", json_headers
|
|
581
|
-
silent_status = last_response.status
|
|
582
|
-
silent_body = last_response.body
|
|
583
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
584
|
-
|
|
585
|
-
stub_development_env
|
|
586
|
-
post "/bucket/opt_out_render", "{}", json_headers
|
|
587
|
-
|
|
588
|
-
expect(Rails.logger).to have_received(:warn).with(a_string_matching(warning_re))
|
|
589
|
-
expect(last_response.status).to eq(silent_status)
|
|
590
|
-
expect(last_response.body).to eq(silent_body)
|
|
591
|
-
expect(JSON.parse(last_response.body)).to eq("ok" => false)
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
it "guardrail (a): silent on the documented Bucket-1 `ruact_errors(record); render :new` pattern" do
|
|
595
|
-
stub_development_env
|
|
596
|
-
post "/bucket/failed_with_page_render", "post[title]=", html_form_headers
|
|
597
|
-
|
|
598
|
-
expect(last_response.status).to eq(200)
|
|
599
|
-
expect(last_response.body).to include("the form again")
|
|
600
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
it "guardrail (b): silent on `ruact_errors(record); redirect_to` on a function-call request" do
|
|
604
|
-
stub_development_env
|
|
605
|
-
post "/bucket/failed_with_redirect", "{}", json_headers
|
|
606
|
-
|
|
607
|
-
expect(JSON.parse(last_response.body)).to eq("$redirect" => "/posts/new")
|
|
608
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
609
|
-
end
|
|
610
|
-
|
|
611
|
-
it "guardrail (b): silent on `ruact_errors(record); redirect_to` on a Bucket-1 request" do
|
|
612
|
-
stub_development_env
|
|
613
|
-
post "/bucket/failed_with_redirect", "post[title]=", html_form_headers
|
|
614
|
-
|
|
615
|
-
expect(last_response.status).to eq(302)
|
|
616
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
617
|
-
end
|
|
618
|
-
|
|
619
|
-
it "guardrail (c): silent on the fall-through itself (errors were injected)" do
|
|
620
|
-
stub_development_env
|
|
621
|
-
post "/bucket/validated_create_invalid", "{}", json_headers
|
|
622
|
-
|
|
623
|
-
expect(JSON.parse(last_response.body).fetch("errors")).to eq("title" => ["Title can't be blank"])
|
|
624
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
it "guardrail (d): silent on an explicit render when the collector was never touched" do
|
|
628
|
-
stub_development_env
|
|
629
|
-
post "/bucket/untouched_explicit_render", "{}", json_headers
|
|
630
|
-
|
|
631
|
-
expect(JSON.parse(last_response.body)).to eq("ok" => true)
|
|
632
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
633
|
-
end
|
|
634
|
-
|
|
635
|
-
it "never warns outside development (the bypass case in the test env stays silent)" do
|
|
636
|
-
stub_env("test")
|
|
637
|
-
post "/bucket/opt_out_render", "{}", json_headers
|
|
638
|
-
|
|
639
|
-
expect(last_response.status).to eq(200)
|
|
640
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
it "never warns in production either" do
|
|
644
|
-
stub_env("production")
|
|
645
|
-
post "/bucket/opt_out_render", "{}", json_headers
|
|
646
|
-
|
|
647
|
-
expect(last_response.status).to eq(200)
|
|
648
|
-
expect(Rails.logger).not_to have_received(:warn).with(a_string_matching(warning_re))
|
|
649
|
-
end
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
# Story 10.0 (AC5) — the Server-path inherits the default_render graceful
|
|
653
|
-
# degradation fix. A GET is never a function call, so Server#default_render
|
|
654
|
-
# delegates to super → the fixed Controller#default_render.
|
|
655
|
-
describe "Story 10.0: Server path inherits non-HTML Accept graceful degradation (AC5)", :story_10_0 do
|
|
656
|
-
it "GET with Accept: */* returns the HTML shell, not a 500" do
|
|
657
|
-
get "/server-implicit/show", {}, { "HTTP_ACCEPT" => "*/*" }
|
|
658
|
-
expect(last_response.status).to(eq(200),
|
|
659
|
-
"expected 200, got #{last_response.status} body=#{last_response.body[0, 400]}")
|
|
660
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
661
|
-
expect(last_response.body).to include("DemoButton")
|
|
662
|
-
expect(last_response.body).not_to include("__ruact_component__ called outside a ruact_render flow")
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
it "GET with Accept: text/x-component returns a Flight payload (function-call path untouched)" do
|
|
666
|
-
get "/server-implicit/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
|
|
667
|
-
expect(last_response.status).to eq(200)
|
|
668
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
669
|
-
end
|
|
670
|
-
end
|
|
671
|
-
|
|
672
|
-
# Story 15.5 (FR109) — the dev-mode response-shape legibility log. One
|
|
673
|
-
# `[ruact]` line per ruact-NEGOTIATED request naming the chosen bucket + the
|
|
674
|
-
# deciding signal. Log-only (byte-identical), dev-gated, SILENT on
|
|
675
|
-
# non-negotiated (plain Rails) responses. Mirrors the F6 warning test shape.
|
|
676
|
-
describe "Story 15.5: dev-mode response-shape log (FR109)", :story_15_5 do
|
|
677
|
-
# The distinctive fragment of the 15.5 log — deliberately distinct from the
|
|
678
|
-
# F6 warning line (which matches `called \`ruact_errors\` and then rendered`).
|
|
679
|
-
let(:shape_log_re) { /\[ruact\] .* — .* → / }
|
|
680
|
-
let(:flight_headers) { { "HTTP_ACCEPT" => "text/x-component" } }
|
|
681
|
-
let(:html_get_headers) { { "HTTP_ACCEPT" => "*/*" } }
|
|
682
|
-
|
|
683
|
-
before do
|
|
684
|
-
# The suite default env is development (RAILS_ENV unset); dev examples
|
|
685
|
-
# still stub it explicitly so they never depend on the suite default.
|
|
686
|
-
allow(Rails.logger).to receive(:info)
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
def stub_env(name)
|
|
690
|
-
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new(name))
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
def stub_development_env
|
|
694
|
-
stub_env("development")
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
it "logs the function-call JSON bucket once with the deciding signal (AC2)" do
|
|
698
|
-
stub_development_env
|
|
699
|
-
post "/bucket/with_ivars", "{}", json_headers
|
|
700
|
-
|
|
701
|
-
expect(Rails.logger).to have_received(:info).with(
|
|
702
|
-
"[ruact] ServerBucketSpecSupport::BucketServerController#with_ivars — " \
|
|
703
|
-
"Accept: application/json + POST → function-call JSON"
|
|
704
|
-
).once
|
|
705
|
-
end
|
|
706
|
-
|
|
707
|
-
it "names the 204 (no ivars) sub-shape" do
|
|
708
|
-
stub_development_env
|
|
709
|
-
post "/bucket/empty", "{}", json_headers
|
|
710
|
-
|
|
711
|
-
expect(last_response.status).to eq(204)
|
|
712
|
-
expect(Rails.logger).to have_received(:info).with(
|
|
713
|
-
a_string_matching(%r{#empty_action — Accept: application/json \+ POST → function-call JSON \(204, no ivars\)})
|
|
714
|
-
).once
|
|
715
|
-
end
|
|
716
|
-
|
|
717
|
-
it "names the $redirect sub-shape" do
|
|
718
|
-
stub_development_env
|
|
719
|
-
post "/bucket/redirecting", "{}", json_headers
|
|
720
|
-
|
|
721
|
-
expect(JSON.parse(last_response.body)).to eq("$redirect" => "/posts/1")
|
|
722
|
-
expect(Rails.logger).to have_received(:info).with(
|
|
723
|
-
a_string_matching(%r{#redirecting — Accept: application/json \+ POST → function-call \$redirect})
|
|
724
|
-
).once
|
|
725
|
-
end
|
|
726
|
-
|
|
727
|
-
it "logs the Flight-page bucket on a text/x-component page render (AC2)" do
|
|
728
|
-
stub_development_env
|
|
729
|
-
get "/server-implicit/show", {}, flight_headers
|
|
730
|
-
|
|
731
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
732
|
-
expect(Rails.logger).to have_received(:info).with(
|
|
733
|
-
"[ruact] ServerBucketSpecSupport::ServerImplicitController#show — " \
|
|
734
|
-
"Accept: text/x-component → Flight page"
|
|
735
|
-
).once
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
it "logs the HTML-shell bucket on an HTML-acceptable page GET (AC2)" do
|
|
739
|
-
stub_development_env
|
|
740
|
-
get "/server-implicit/show", {}, html_get_headers
|
|
741
|
-
|
|
742
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
743
|
-
expect(Rails.logger).to have_received(:info).with(
|
|
744
|
-
"[ruact] ServerBucketSpecSupport::ServerImplicitController#show — " \
|
|
745
|
-
"HTML-acceptable Accept, no Flight header → HTML shell"
|
|
746
|
-
).once
|
|
747
|
-
end
|
|
748
|
-
|
|
749
|
-
it "is SILENT on a plain Rails action on the same controller (non-negotiated, AC2)" do
|
|
750
|
-
stub_development_env
|
|
751
|
-
get "/bucket/plain_rails"
|
|
752
|
-
|
|
753
|
-
expect(last_response.status).to eq(200)
|
|
754
|
-
expect(last_response.body).to eq("just rails")
|
|
755
|
-
expect(Rails.logger).not_to have_received(:info).with(a_string_matching(shape_log_re))
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
it "is SILENT outside development and byte-identical to the dev response (log-only, AC2)",
|
|
759
|
-
:aggregate_failures do
|
|
760
|
-
stub_env("production")
|
|
761
|
-
post "/bucket/with_ivars", "{}", json_headers
|
|
762
|
-
prod_status = last_response.status
|
|
763
|
-
prod_body = last_response.body
|
|
764
|
-
prod_ctype = last_response.headers["Content-Type"]
|
|
765
|
-
prod_vary = last_response.headers["Vary"]
|
|
766
|
-
expect(Rails.logger).not_to have_received(:info).with(a_string_matching(shape_log_re))
|
|
767
|
-
|
|
768
|
-
stub_development_env
|
|
769
|
-
post "/bucket/with_ivars", "{}", json_headers
|
|
770
|
-
expect(Rails.logger).to have_received(:info).with(a_string_matching(shape_log_re))
|
|
771
|
-
expect(last_response.status).to eq(prod_status)
|
|
772
|
-
expect(last_response.body).to eq(prod_body)
|
|
773
|
-
expect(last_response.headers["Content-Type"]).to eq(prod_ctype)
|
|
774
|
-
expect(last_response.headers["Vary"]).to eq(prod_vary)
|
|
775
|
-
end
|
|
776
|
-
|
|
777
|
-
it "never logs in the test environment either" do
|
|
778
|
-
stub_env("test")
|
|
779
|
-
post "/bucket/with_ivars", "{}", json_headers
|
|
780
|
-
|
|
781
|
-
expect(last_response.status).to eq(200)
|
|
782
|
-
expect(Rails.logger).not_to have_received(:info).with(a_string_matching(shape_log_re))
|
|
783
|
-
end
|
|
784
|
-
end
|
|
785
|
-
end
|