ruact 0.0.5 → 0.0.7
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/.github/workflows/ci.yml +54 -2
- data/.rubocop_todo.yml +3 -115
- data/CHANGELOG.md +74 -18
- data/bench/server_functions_dispatch_bench.rb +109 -142
- data/bench/server_functions_dispatch_bench.results.md +29 -0
- data/docs/internal/decisions/server-functions-api.md +402 -0
- data/lib/generators/ruact/install/install_generator.rb +310 -25
- data/lib/generators/ruact/install/templates/Procfile.dev.tt +2 -0
- data/lib/generators/ruact/install/templates/dev.tt +16 -0
- data/lib/generators/ruact/install/templates/package.json.tt +17 -0
- data/lib/generators/ruact/install/templates/vite.config.js.tt +3 -1
- data/lib/generators/ruact/scaffold/scaffold_attribute.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_form_helpers.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_generator.rb +491 -0
- data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +229 -0
- data/lib/generators/ruact/scaffold/templates/components/DeleteDialog.tsx.tt +104 -0
- data/lib/generators/ruact/scaffold/templates/components/Form.tsx.tt +212 -0
- data/lib/generators/ruact/scaffold/templates/components/List.tsx.tt +338 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/DeleteDialog.tsx.tt +92 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/Form.tsx.tt +174 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/List.tsx.tt +253 -0
- data/lib/generators/ruact/scaffold/templates/controller.rb.tt +130 -0
- data/lib/generators/ruact/scaffold/templates/queries/application_query.rb.tt +13 -0
- data/lib/generators/ruact/scaffold/templates/queries/query.rb.tt +59 -0
- data/lib/generators/ruact/scaffold/templates/request_spec.rb.tt +77 -0
- data/lib/generators/ruact/scaffold/templates/views/edit.html.erb.tt +7 -0
- data/lib/generators/ruact/scaffold/templates/views/index.html.erb.tt +6 -0
- data/lib/generators/ruact/scaffold/templates/views/new.html.erb.tt +5 -0
- data/lib/generators/ruact/scaffold/templates/views/show.html.erb.tt +16 -0
- data/lib/ruact/client_manifest.rb +37 -36
- data/lib/ruact/component_contract.rb +115 -0
- data/lib/ruact/configuration.rb +80 -28
- data/lib/ruact/controller.rb +79 -425
- data/lib/ruact/doctor.rb +133 -4
- data/lib/ruact/erb_preprocessor.rb +71 -12
- data/lib/ruact/erb_preprocessor_hook.rb +4 -1
- data/lib/ruact/errors.rb +30 -44
- data/lib/ruact/html_converter.rb +22 -1
- data/lib/ruact/manifest_resolver.rb +149 -0
- data/lib/ruact/railtie.rb +70 -203
- data/lib/ruact/render_pipeline.rb +8 -1
- data/lib/ruact/server.rb +28 -6
- data/lib/ruact/server_functions/codegen.rb +49 -188
- data/lib/ruact/server_functions/codegen_v2.rb +23 -6
- data/lib/ruact/server_functions/codegen_v2_query_params.rb +140 -0
- data/lib/ruact/server_functions/error_payload.rb +1 -1
- data/lib/ruact/server_functions/error_rendering.rb +22 -29
- data/lib/ruact/server_functions/name_bridge.rb +14 -16
- data/lib/ruact/server_functions/query_source.rb +35 -8
- data/lib/ruact/server_functions/route_source.rb +3 -4
- data/lib/ruact/server_functions/snapshot.rb +12 -139
- data/lib/ruact/server_functions/validation_errors.rb +70 -0
- data/lib/ruact/server_functions.rb +21 -25
- data/lib/ruact/signed_references.rb +162 -0
- data/lib/ruact/string_distance.rb +72 -0
- data/lib/ruact/validation_errors_collector.rb +139 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +109 -0
- data/lib/ruact.rb +20 -19
- data/lib/tasks/ruact.rake +10 -53
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +36 -0
- data/spec/ruact/component_contract_spec.rb +119 -0
- data/spec/ruact/configuration_spec.rb +51 -34
- data/spec/ruact/controller_request_spec.rb +264 -0
- data/spec/ruact/controller_spec.rb +70 -328
- data/spec/ruact/doctor_spec.rb +201 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +4 -1
- data/spec/ruact/erb_preprocessor_spec.rb +127 -0
- data/spec/ruact/errors_spec.rb +0 -45
- data/spec/ruact/html_converter_spec.rb +50 -0
- data/spec/ruact/install_generator_spec.rb +591 -4
- data/spec/ruact/manifest_resolver_spec.rb +159 -0
- data/spec/ruact/query_request_spec.rb +109 -1
- data/spec/ruact/scaffold_generator_spec.rb +1835 -0
- data/spec/ruact/server_bucket_request_spec.rb +142 -0
- data/spec/ruact/server_function_name_spec.rb +1 -1
- data/spec/ruact/server_functions/codegen_spec.rb +158 -269
- data/spec/ruact/server_functions/name_bridge_spec.rb +9 -9
- data/spec/ruact/server_functions/query_source_spec.rb +51 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +71 -268
- data/spec/ruact/server_functions/rake_spec.rb +29 -29
- data/spec/ruact/server_functions/snapshot_spec.rb +53 -213
- data/spec/ruact/server_rescue_request_spec.rb +6 -6
- data/spec/ruact/server_spec.rb +8 -9
- data/spec/ruact/signed_references_spec.rb +164 -0
- data/spec/ruact/string_distance_spec.rb +38 -0
- data/spec/ruact/validation_errors_spec.rb +116 -0
- data/spec/ruact/view_helper_spec.rb +79 -0
- data/spec/spec_helper.rb +15 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +40 -45
- data/vendor/javascript/ruact-server-functions-runtime/index.js +42 -98
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
- data/vendor/javascript/ruact-server-functions-runtime/package.json +1 -1
- data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +372 -6
- data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
- data/vendor/javascript/vite-plugin-ruact/manifest-http.test.mjs +125 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
- data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
- data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
- metadata +58 -15
- data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
- data/lib/ruact/server_action.rb +0 -131
- data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
- data/lib/ruact/server_functions/registry.rb +0 -148
- data/lib/ruact/server_functions/registry_entry.rb +0 -26
- data/lib/ruact/server_functions/standalone_context.rb +0 -103
- data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
- data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
- data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
- data/spec/ruact/server_functions/registry_spec.rb +0 -199
- data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
- data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
module Ruact
|
|
8
|
+
RSpec.describe ManifestResolver do
|
|
9
|
+
# spec_helper pins resolve/resolve_soft to the boot manifest suite-wide for
|
|
10
|
+
# hermeticity; this spec is the one place that exercises the REAL paths.
|
|
11
|
+
before do
|
|
12
|
+
allow(described_class).to receive(:resolve).and_call_original
|
|
13
|
+
allow(described_class).to receive(:resolve_soft).and_call_original
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:manifest_hash) do
|
|
17
|
+
{
|
|
18
|
+
"LikeButton" => {
|
|
19
|
+
"id" => "/LikeButton.jsx",
|
|
20
|
+
"name" => "LikeButton",
|
|
21
|
+
"chunks" => ["/LikeButton.jsx"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
let(:manifest_json) { JSON.generate(manifest_hash) }
|
|
26
|
+
|
|
27
|
+
# A real Net::HTTPSuccess (HTTPOK < HTTPSuccess) whose body we override, so
|
|
28
|
+
# the resolver's `response.is_a?(Net::HTTPSuccess)` branch is exercised for
|
|
29
|
+
# real without touching the network.
|
|
30
|
+
def http_ok(body)
|
|
31
|
+
response = Net::HTTPOK.new("1.1", "200", "OK")
|
|
32
|
+
response.define_singleton_method(:body) { body }
|
|
33
|
+
response
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def in_dev
|
|
37
|
+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development"))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe ".resolve in development (the boot-race)" do
|
|
41
|
+
before do
|
|
42
|
+
in_dev
|
|
43
|
+
# Simulate the race: boot read the missing file, so the cached manifest is nil.
|
|
44
|
+
allow(Ruact).to receive(:manifest).and_return(nil)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "resolves from the Vite dev server over HTTP (no 500, ref resolvable)" do
|
|
48
|
+
allow(Net::HTTP).to receive(:start).and_return(http_ok(manifest_json))
|
|
49
|
+
|
|
50
|
+
manifest = described_class.resolve
|
|
51
|
+
|
|
52
|
+
expect(manifest).to be_a(ClientManifest)
|
|
53
|
+
ref = manifest.reference_for("LikeButton")
|
|
54
|
+
expect(ref.module_id).to eq("/LikeButton.jsx")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "fetches the manifest from the configured vite_dev_server URL" do
|
|
58
|
+
allow(Net::HTTP).to receive(:start).and_return(http_ok(manifest_json))
|
|
59
|
+
|
|
60
|
+
described_class.resolve
|
|
61
|
+
|
|
62
|
+
expect(Net::HTTP).to have_received(:start).with(
|
|
63
|
+
"localhost", 5173, hash_including(:open_timeout, :read_timeout)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "does not double-slash the path when vite_dev_server has a trailing slash" do
|
|
68
|
+
allow(described_class).to receive(:base_url).and_return("http://localhost:5173/")
|
|
69
|
+
http = instance_double(Net::HTTP)
|
|
70
|
+
allow(http).to receive(:get).and_return(http_ok(manifest_json))
|
|
71
|
+
allow(Net::HTTP).to receive(:start).and_yield(http).and_return(http_ok(manifest_json))
|
|
72
|
+
|
|
73
|
+
described_class.resolve
|
|
74
|
+
|
|
75
|
+
expect(http).to have_received(:get).with("/__ruact/manifest")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "speaks TLS when vite_dev_server is https" do
|
|
79
|
+
allow(described_class).to receive(:base_url).and_return("https://localhost:5173")
|
|
80
|
+
allow(Net::HTTP).to receive(:start).and_return(http_ok(manifest_json))
|
|
81
|
+
|
|
82
|
+
described_class.resolve
|
|
83
|
+
|
|
84
|
+
expect(Net::HTTP).to have_received(:start).with(
|
|
85
|
+
"localhost", 5173, hash_including(use_ssl: true)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "falls back to the on-disk file when the dev server is unreachable" do
|
|
90
|
+
allow(Net::HTTP).to receive(:start).and_raise(Errno::ECONNREFUSED)
|
|
91
|
+
|
|
92
|
+
Dir.mktmpdir do |dir|
|
|
93
|
+
path = File.join(dir, "react-client-manifest.json")
|
|
94
|
+
File.write(path, manifest_json)
|
|
95
|
+
allow(described_class).to receive(:file_path).and_return(path)
|
|
96
|
+
|
|
97
|
+
manifest = described_class.resolve
|
|
98
|
+
expect(manifest).to be_a(ClientManifest)
|
|
99
|
+
expect(manifest.reference_for("LikeButton").module_id).to eq("/LikeButton.jsx")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "raises a clear, actionable error (not NoMethodError) when neither HTTP nor file is available" do
|
|
104
|
+
allow(Net::HTTP).to receive(:start).and_raise(Errno::ECONNREFUSED)
|
|
105
|
+
allow(described_class).to receive(:file_path).and_return("/no/such/manifest.json")
|
|
106
|
+
|
|
107
|
+
expect { described_class.resolve }.to raise_error(ManifestError, %r{Vite dev server.*bin/dev}m)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "ignores a non-2xx dev-server response and falls back" do
|
|
111
|
+
allow(Net::HTTP).to receive(:start).and_return(Net::HTTPNotFound.new("1.1", "404", "Not Found"))
|
|
112
|
+
allow(described_class).to receive(:file_path).and_return("/no/such/manifest.json")
|
|
113
|
+
|
|
114
|
+
expect { described_class.resolve }.to raise_error(ManifestError)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe ".resolve_soft in development (fail-open for FR100)" do
|
|
119
|
+
before do
|
|
120
|
+
in_dev
|
|
121
|
+
allow(Ruact).to receive(:manifest).and_return(nil)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "returns nil (no raise) when nothing is resolvable" do
|
|
125
|
+
allow(Net::HTTP).to receive(:start).and_raise(Errno::ECONNREFUSED)
|
|
126
|
+
allow(described_class).to receive(:file_path).and_return("/no/such/manifest.json")
|
|
127
|
+
|
|
128
|
+
expect(described_class.resolve_soft).to be_nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "returns the dev manifest when the server is up" do
|
|
132
|
+
allow(Net::HTTP).to receive(:start).and_return(http_ok(manifest_json))
|
|
133
|
+
expect(described_class.resolve_soft).to be_a(ClientManifest)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "production / non-development (untouched)" do
|
|
138
|
+
it "returns Ruact.manifest verbatim without any HTTP call" do
|
|
139
|
+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
|
|
140
|
+
allow(Net::HTTP).to receive(:start)
|
|
141
|
+
boot_manifest = ClientManifest.from_hash(manifest_hash)
|
|
142
|
+
allow(Ruact).to receive(:manifest).and_return(boot_manifest)
|
|
143
|
+
|
|
144
|
+
expect(described_class.resolve).to be(boot_manifest)
|
|
145
|
+
expect(Net::HTTP).not_to have_received(:start)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "returns Ruact.manifest in the test environment too (no dev fetch)" do
|
|
149
|
+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("test"))
|
|
150
|
+
allow(Net::HTTP).to receive(:start)
|
|
151
|
+
boot_manifest = ClientManifest.from_hash(manifest_hash)
|
|
152
|
+
allow(Ruact).to receive(:manifest).and_return(boot_manifest)
|
|
153
|
+
|
|
154
|
+
expect(described_class.resolve).to be(boot_manifest)
|
|
155
|
+
expect(Net::HTTP).not_to have_received(:start)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -22,6 +22,10 @@ require "action_view/railtie"
|
|
|
22
22
|
|
|
23
23
|
require "spec_helper"
|
|
24
24
|
require "rack/test"
|
|
25
|
+
require "global_id"
|
|
26
|
+
require "active_support/core_ext/integer/time"
|
|
27
|
+
require "active_support/core_ext/numeric/time"
|
|
28
|
+
require "active_support/message_verifier"
|
|
25
29
|
|
|
26
30
|
require "ruact/controller"
|
|
27
31
|
require "ruact/server"
|
|
@@ -183,6 +187,41 @@ module QueryRequestSpecSupport # rubocop:disable Style/OneClassPerFile
|
|
|
183
187
|
{ "opt" => opt }
|
|
184
188
|
end
|
|
185
189
|
end
|
|
190
|
+
|
|
191
|
+
# Story 13.2 (FR96) — a GlobalID-locatable record used to exercise the signed
|
|
192
|
+
# reference round-trip through the real host request cycle.
|
|
193
|
+
class RefPost
|
|
194
|
+
include GlobalID::Identification
|
|
195
|
+
|
|
196
|
+
attr_reader :id
|
|
197
|
+
|
|
198
|
+
def initialize(id)
|
|
199
|
+
@id = id.to_s
|
|
200
|
+
self.class.store[@id] = self
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.store
|
|
204
|
+
@store ||= {}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def self.find(id)
|
|
208
|
+
store.fetch(id.to_s)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Story 13.2 (FR96) — a public query (skips auth) that resolves a signed
|
|
213
|
+
# reference handed back from the client. A tampered/expired/wrong-purpose
|
|
214
|
+
# token raises Ruact::InvalidSignedGlobalIDError, which the Story 8.4
|
|
215
|
+
# structured-error chain renders as a clean 400 (no ActiveRecord::RecordNotFound
|
|
216
|
+
# leak, no raw-id trust).
|
|
217
|
+
class SignedRefQuery < ApplicationQuery
|
|
218
|
+
ruact_skip_before_action :require_login
|
|
219
|
+
|
|
220
|
+
def resolve_ref(token:)
|
|
221
|
+
record = Ruact.locate_signed(token, for: :spec_ref)
|
|
222
|
+
{ "id" => record.id }
|
|
223
|
+
end
|
|
224
|
+
end
|
|
186
225
|
end
|
|
187
226
|
|
|
188
227
|
if defined?(ControllerRequestSpecSupport) &&
|
|
@@ -191,7 +230,8 @@ if defined?(ControllerRequestSpecSupport) &&
|
|
|
191
230
|
ControllerRequestSpecSupport.app_class.routes.append do
|
|
192
231
|
ruact_queries QueryRequestSpecSupport::CatalogQuery,
|
|
193
232
|
QueryRequestSpecSupport::PublicCatalogQuery,
|
|
194
|
-
QueryRequestSpecSupport::SearchQuery
|
|
233
|
+
QueryRequestSpecSupport::SearchQuery,
|
|
234
|
+
QueryRequestSpecSupport::SignedRefQuery
|
|
195
235
|
end
|
|
196
236
|
end
|
|
197
237
|
|
|
@@ -596,3 +636,71 @@ RSpec.describe "Story 9.5: FR88 query kwargs sanitization", :story_9_5 do
|
|
|
596
636
|
end
|
|
597
637
|
end
|
|
598
638
|
end
|
|
639
|
+
|
|
640
|
+
# Story 13.2 (FR96) — SignedGlobalID references resolved through the REAL host
|
|
641
|
+
# request cycle: a valid token round-trips; a tampered token surfaces as a clean
|
|
642
|
+
# 400 via the Story 8.4 structured-error chain (no ActiveRecord::RecordNotFound
|
|
643
|
+
# leak, no raw-id trust).
|
|
644
|
+
RSpec.describe "Story 13.2: SignedGlobalID record references (FR96)", :story_13_2 do
|
|
645
|
+
include Rack::Test::Methods
|
|
646
|
+
|
|
647
|
+
let(:app_class) { ControllerRequestSpecSupport.app_class }
|
|
648
|
+
let(:app) { app_class.instance }
|
|
649
|
+
|
|
650
|
+
before do
|
|
651
|
+
Rails.logger = Logger.new(IO::NULL)
|
|
652
|
+
ControllerRequestSpecSupport.boot!
|
|
653
|
+
# globalid signing context for the booted app (the 7.9 app sets
|
|
654
|
+
# secret_key_base but does not load the globalid railtie).
|
|
655
|
+
GlobalID.app = "ruact-test"
|
|
656
|
+
SignedGlobalID.verifier = ActiveSupport::MessageVerifier.new("a" * 64)
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def sign(record, purpose: :spec_ref, expires_in: 1.hour)
|
|
660
|
+
Ruact.signed_global_id(record, for: purpose, expires_in: expires_in)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it "round-trips a valid token through the host chain (no auth needed — public query)" do
|
|
664
|
+
record = QueryRequestSpecSupport::RefPost.new("42")
|
|
665
|
+
get "/q/resolveRef", { "token" => sign(record) }
|
|
666
|
+
expect(last_response.status).to(eq(200), "body=#{last_response.body[0, 300]}")
|
|
667
|
+
expect(JSON.parse(last_response.body)).to eq("id" => "42")
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
it "rejects a tampered token as a clean 400 structured error (no RecordNotFound leak)" do
|
|
671
|
+
record = QueryRequestSpecSupport::RefPost.new("99")
|
|
672
|
+
get "/q/resolveRef", { "token" => "#{sign(record)}tamper" }
|
|
673
|
+
|
|
674
|
+
expect(last_response.status).to eq(400)
|
|
675
|
+
body = JSON.parse(last_response.body)
|
|
676
|
+
expect(body.fetch("_ruact_server_action_error")).to be(true)
|
|
677
|
+
expect(body.fetch("action_name")).to eq("resolve_ref")
|
|
678
|
+
expect(body.fetch("error_class")).to eq("Ruact::InvalidSignedGlobalIDError")
|
|
679
|
+
expect(last_response.body).not_to include("RecordNotFound")
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
it "rejects a wrong-purpose token as a clean 400" do
|
|
683
|
+
record = QueryRequestSpecSupport::RefPost.new("7")
|
|
684
|
+
get "/q/resolveRef", { "token" => sign(record, purpose: :other_purpose) }
|
|
685
|
+
expect(last_response.status).to eq(400)
|
|
686
|
+
expect(JSON.parse(last_response.body).fetch("error_class"))
|
|
687
|
+
.to eq("Ruact::InvalidSignedGlobalIDError")
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
it "does not leak dev-only error fields in production payload mode" do
|
|
691
|
+
record = QueryRequestSpecSupport::RefPost.new("5")
|
|
692
|
+
Ruact.configure { |c| c.dev_error_payload_enabled = false }
|
|
693
|
+
get "/q/resolveRef", { "token" => "#{sign(record)}tamper" }
|
|
694
|
+
expect(last_response.status).to eq(400)
|
|
695
|
+
body = JSON.parse(last_response.body)
|
|
696
|
+
# production payload is exactly the four baseline keys — no split backtrace,
|
|
697
|
+
# suggestion, or validation/upload blocks (the real dev-only field names).
|
|
698
|
+
expect(body.keys).to contain_exactly(
|
|
699
|
+
"_ruact_server_action_error", "action_name", "error_class", "message"
|
|
700
|
+
)
|
|
701
|
+
expect(body.keys).not_to include("app_frames", "gem_frames", "suggestion")
|
|
702
|
+
ensure
|
|
703
|
+
Ruact.instance_variable_set(:@config, nil)
|
|
704
|
+
Ruact.instance_variable_set(:@configured_at_least_once, false)
|
|
705
|
+
end
|
|
706
|
+
end
|