ruact 0.0.2 → 0.0.4
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/.codecov.yml +31 -0
- data/.github/workflows/ci.yml +160 -94
- data/.github/workflows/server-functions-bench.yml +54 -0
- data/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +175 -0
- data/CHANGELOG.md +88 -5
- data/README.md +2 -0
- data/RELEASING.md +9 -3
- data/bench/server_functions_dispatch_bench.rb +309 -0
- data/bench/server_functions_dispatch_bench.results.md +121 -0
- data/docs/internal/README.md +9 -0
- data/docs/internal/decisions/server-functions-api.md +1779 -0
- data/lib/generators/ruact/install/install_generator.rb +43 -0
- data/lib/generators/ruact/install/templates/application.jsx.tt +1 -1
- data/lib/generators/ruact/install/templates/initializer.rb.tt +1 -1
- data/lib/ruact/client_manifest.rb +125 -12
- data/lib/ruact/configuration.rb +264 -23
- data/lib/ruact/controller.rb +459 -32
- data/lib/ruact/doctor.rb +34 -2
- data/lib/ruact/erb_preprocessor.rb +6 -6
- data/lib/ruact/errors.rb +100 -0
- data/lib/ruact/flight/serializer.rb +2 -2
- data/lib/ruact/html_converter.rb +131 -31
- data/lib/ruact/query.rb +107 -0
- data/lib/ruact/railtie.rb +220 -3
- data/lib/ruact/render_context.rb +30 -0
- data/lib/ruact/render_pipeline.rb +201 -59
- data/lib/ruact/routing.rb +81 -0
- data/lib/ruact/serializable.rb +11 -11
- data/lib/ruact/server.rb +341 -0
- data/lib/ruact/server_action.rb +131 -0
- data/lib/ruact/server_functions/backtrace_cleaner.rb +32 -0
- data/lib/ruact/server_functions/bucket_two_payload.rb +109 -0
- data/lib/ruact/server_functions/codegen.rb +344 -0
- data/lib/ruact/server_functions/codegen_v2.rb +212 -0
- data/lib/ruact/server_functions/endpoint_controller.rb +237 -0
- data/lib/ruact/server_functions/error_payload.rb +93 -0
- data/lib/ruact/server_functions/error_rendering.rb +190 -0
- data/lib/ruact/server_functions/error_suggestion.rb +38 -0
- data/lib/ruact/server_functions/name_bridge.rb +118 -0
- data/lib/ruact/server_functions/query_context.rb +62 -0
- data/lib/ruact/server_functions/query_dispatch.rb +313 -0
- data/lib/ruact/server_functions/query_source.rb +150 -0
- data/lib/ruact/server_functions/registry.rb +148 -0
- data/lib/ruact/server_functions/registry_entry.rb +26 -0
- data/lib/ruact/server_functions/route_source.rb +201 -0
- data/lib/ruact/server_functions/snapshot.rb +195 -0
- data/lib/ruact/server_functions/snapshot_writer.rb +65 -0
- data/lib/ruact/server_functions/standalone_context.rb +103 -0
- data/lib/ruact/server_functions/standalone_dispatcher.rb +178 -0
- data/lib/ruact/server_functions.rb +111 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +17 -9
- data/lib/ruact.rb +85 -6
- data/lib/rubocop/cop/ruact/no_shared_state.rb +1 -1
- data/lib/tasks/benchmark.rake +15 -11
- data/lib/tasks/ruact.rake +81 -0
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +1 -1
- data/spec/fixtures/flight/README.md +55 -7
- data/spec/fixtures/flight/bigint.txt +1 -0
- data/spec/fixtures/flight/infinity.txt +1 -0
- data/spec/fixtures/flight/nan.txt +1 -0
- data/spec/fixtures/flight/negative_infinity.txt +1 -0
- data/spec/fixtures/flight/undefined.txt +1 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +108 -0
- data/spec/ruact/configuration_spec.rb +501 -0
- data/spec/ruact/controller_request_spec.rb +204 -0
- data/spec/ruact/controller_spec.rb +427 -39
- data/spec/ruact/doctor_spec.rb +118 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +3 -3
- data/spec/ruact/erb_preprocessor_spec.rb +7 -7
- data/spec/ruact/errors_spec.rb +95 -0
- data/spec/ruact/flight/renderer_spec.rb +14 -3
- data/spec/ruact/flight/serializer_spec.rb +129 -88
- data/spec/ruact/html_converter_spec.rb +183 -5
- data/spec/ruact/install_generator_spec.rb +93 -0
- data/spec/ruact/query_request_spec.rb +598 -0
- data/spec/ruact/query_spec.rb +105 -0
- data/spec/ruact/railtie_spec.rb +2 -3
- data/spec/ruact/render_context_spec.rb +58 -0
- data/spec/ruact/render_pipeline_concurrency_spec.rb +78 -0
- data/spec/ruact/render_pipeline_spec.rb +784 -330
- data/spec/ruact/serializable_spec.rb +8 -8
- data/spec/ruact/server_bucket_request_spec.rb +352 -0
- data/spec/ruact/server_function_name_spec.rb +53 -0
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +63 -0
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +200 -0
- data/spec/ruact/server_functions/codegen_spec.rb +508 -0
- data/spec/ruact/server_functions/csrf_request_spec.rb +380 -0
- data/spec/ruact/server_functions/dispatch_request_spec.rb +819 -0
- data/spec/ruact/server_functions/error_payload_spec.rb +222 -0
- data/spec/ruact/server_functions/error_suggestion_spec.rb +79 -0
- data/spec/ruact/server_functions/name_bridge_spec.rb +212 -0
- data/spec/ruact/server_functions/query_context_spec.rb +72 -0
- data/spec/ruact/server_functions/query_source_spec.rb +142 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +412 -0
- data/spec/ruact/server_functions/rake_spec.rb +86 -0
- data/spec/ruact/server_functions/registry_spec.rb +199 -0
- data/spec/ruact/server_functions/route_source_spec.rb +202 -0
- data/spec/ruact/server_functions/snapshot_spec.rb +256 -0
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +71 -0
- data/spec/ruact/server_functions/standalone_action_spec.rb +224 -0
- data/spec/ruact/server_functions/standalone_context_spec.rb +142 -0
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +273 -0
- data/spec/ruact/server_rescue_request_spec.rb +416 -0
- data/spec/ruact/server_spec.rb +180 -0
- data/spec/ruact/server_upload_request_spec.rb +311 -0
- data/spec/ruact/view_helper_spec.rb +23 -17
- data/spec/spec_helper.rb +52 -1
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +135 -0
- data/spec/support/flight_wire_parser_spec.rb +93 -0
- data/spec/support/matchers/flight_fixture_matcher.rb +356 -0
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +250 -0
- data/spec/support/rails_stub.rb +75 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +173 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +614 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +827 -0
- data/vendor/javascript/ruact-server-functions-runtime/package.json +29 -0
- data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +181 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +164 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +1429 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +15 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +804 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +961 -0
- data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +21 -0
- metadata +91 -5
- data/lib/ruact/component_registry.rb +0 -31
- data/lib/tasks/rsc.rake +0 -9
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Ruact
|
|
7
|
+
module ServerFunctions
|
|
8
|
+
RSpec.describe SnapshotWriter, :story_8_0a do
|
|
9
|
+
around do |example|
|
|
10
|
+
Dir.mktmpdir do |dir|
|
|
11
|
+
@tmpdir = dir
|
|
12
|
+
example.run
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:path) { File.join(@tmpdir, "out.txt") }
|
|
17
|
+
|
|
18
|
+
describe ".write_if_changed! (Story 8.0a — atomic, byte-aware writer)" do
|
|
19
|
+
it "writes the file when it does not yet exist and returns true" do
|
|
20
|
+
expect(described_class.write_if_changed!(path: path, content: "hello\n"))
|
|
21
|
+
.to be(true)
|
|
22
|
+
expect(File.read(path)).to eq("hello\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "skips writing when the existing content matches byte-for-byte" do
|
|
26
|
+
File.write(path, "hello\n")
|
|
27
|
+
original_mtime = File.mtime(path)
|
|
28
|
+
sleep 1.05
|
|
29
|
+
|
|
30
|
+
expect(described_class.write_if_changed!(path: path, content: "hello\n"))
|
|
31
|
+
.to be(false)
|
|
32
|
+
expect(File.mtime(path)).to eq(original_mtime)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "writes when the existing content differs by even a single byte" do
|
|
36
|
+
File.write(path, "hello\n")
|
|
37
|
+
expect(described_class.write_if_changed!(path: path, content: "hello!\n"))
|
|
38
|
+
.to be(true)
|
|
39
|
+
expect(File.read(path)).to eq("hello!\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "creates missing parent directories" do
|
|
43
|
+
nested = File.join(@tmpdir, "a", "b", "c", "out.txt")
|
|
44
|
+
described_class.write_if_changed!(path: nested, content: "x")
|
|
45
|
+
expect(File.read(nested)).to eq("x")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "writes via a same-directory tmpfile so partial reads never see " \
|
|
49
|
+
"a torn file (Story 8.0a)" do
|
|
50
|
+
described_class.write_if_changed!(path: path, content: "atomic\n")
|
|
51
|
+
# After the write the temp sibling must not linger.
|
|
52
|
+
siblings = Dir.children(@tmpdir)
|
|
53
|
+
expect(siblings).to eq(["out.txt"])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "raises Ruact::ConfigurationError when the parent directory is unwritable " \
|
|
57
|
+
"(Story 8.0a)" do
|
|
58
|
+
read_only = File.join(@tmpdir, "ro")
|
|
59
|
+
FileUtils.mkdir_p(read_only)
|
|
60
|
+
FileUtils.chmod(0o500, read_only)
|
|
61
|
+
target = File.join(read_only, "nested", "out.txt")
|
|
62
|
+
|
|
63
|
+
expect { described_class.write_if_changed!(path: target, content: "x") }
|
|
64
|
+
.to raise_error(Ruact::ConfigurationError, /cannot create/)
|
|
65
|
+
ensure
|
|
66
|
+
FileUtils.chmod(0o700, read_only) if read_only && File.exist?(read_only)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Story 8.3 — covers `Ruact::ServerAction` extended onto a Module: AC1
|
|
4
|
+
# (registration shape + no method defined on host module) and AC6 (guard-
|
|
5
|
+
# rail matrix mirroring the controller-DSL path adapted to module context).
|
|
6
|
+
|
|
7
|
+
require "spec_helper"
|
|
8
|
+
|
|
9
|
+
RSpec.describe Ruact::ServerAction, :story_8_3 do
|
|
10
|
+
describe "AC1 — extend Ruact::ServerAction + ruact_action registers a standalone host" do
|
|
11
|
+
it "registers a standalone host in Ruact.action_registry with controller: <the Module>" do
|
|
12
|
+
mod = Module.new do
|
|
13
|
+
extend Ruact::ServerAction
|
|
14
|
+
|
|
15
|
+
def self.name
|
|
16
|
+
"AC1RegistrationModule"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ruact_action(:standalone_create_post) { |_params| { ok: true } }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
entries = Ruact.action_registry.entries
|
|
23
|
+
expect(entries).to include(:standalone_create_post)
|
|
24
|
+
|
|
25
|
+
entry = entries[:standalone_create_post]
|
|
26
|
+
expect(entry).to be_a(Ruact::ServerFunctions::RegistryEntry)
|
|
27
|
+
expect(entry.kind).to eq(:action)
|
|
28
|
+
expect(entry.controller).to be(mod)
|
|
29
|
+
expect(entry.controller).to be_a(Module)
|
|
30
|
+
expect(entry.controller).not_to be_a(Class)
|
|
31
|
+
expect(entry.js_identifier).to eq("standaloneCreatePost")
|
|
32
|
+
expect(entry.block).to be_a(Proc)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "does NOT define an instance method on the host module — the block is reachable " \
|
|
36
|
+
"only through the gem endpoint, never as a Ruby method" do
|
|
37
|
+
mod = Module.new do
|
|
38
|
+
extend Ruact::ServerAction
|
|
39
|
+
|
|
40
|
+
def self.name
|
|
41
|
+
"AC1NoMethodModule"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
ruact_action(:no_method_exposed) { |_params| nil }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
expect(mod.respond_to?(:no_method_exposed)).to be(false)
|
|
48
|
+
expect(mod.instance_methods).not_to include(:no_method_exposed)
|
|
49
|
+
expect(mod.singleton_methods).not_to include(:no_method_exposed)
|
|
50
|
+
# Direct dispatch attempts (someone trying to `Mod.send(:no_method_exposed)`
|
|
51
|
+
# should fail with NoMethodError) — proves the surface is endpoint-only.
|
|
52
|
+
expect { mod.send(:no_method_exposed, {}) }.to raise_error(NoMethodError)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "AC6 — guard-rail matrix (mirror controller-DSL adapted to module context)" do
|
|
57
|
+
let(:host) do
|
|
58
|
+
Module.new do
|
|
59
|
+
extend Ruact::ServerAction
|
|
60
|
+
|
|
61
|
+
def self.name
|
|
62
|
+
"GuardRailModule"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "raises ArgumentError when given a String instead of a Symbol" do
|
|
68
|
+
expect do
|
|
69
|
+
host.module_eval { ruact_action("create_post") { |_p| nil } }
|
|
70
|
+
end.to raise_error(ArgumentError, /ruact_action requires a Symbol/)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "raises ArgumentError when the block is missing" do
|
|
74
|
+
expect { host.module_eval { ruact_action(:create_post) } }
|
|
75
|
+
.to raise_error(ArgumentError, /requires a block/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "raises ArgumentError when the block accepts no positional argument" do
|
|
79
|
+
expect do
|
|
80
|
+
host.module_eval { ruact_action(:create_post) {} } # no positional arg
|
|
81
|
+
end.to raise_error(ArgumentError, /exactly one positional parameter/)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "raises ArgumentError when the block accepts more than one positional argument" do
|
|
85
|
+
expect do
|
|
86
|
+
host.module_eval { ruact_action(:create_post) { |_a, _b| nil } }
|
|
87
|
+
end.to raise_error(ArgumentError, /exactly one positional parameter/)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "raises ArgumentError when the block has a required keyword argument" do
|
|
91
|
+
block_with_kwarg = ->(_p, required:) { required }
|
|
92
|
+
expect do
|
|
93
|
+
host.module_eval { ruact_action(:create_post, &block_with_kwarg) }
|
|
94
|
+
end.to raise_error(ArgumentError, /no required keyword arguments/)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "ACCEPTS a block with a single positional arg" do
|
|
98
|
+
expect do
|
|
99
|
+
host.module_eval { ruact_action(:create_post) { |_p| nil } }
|
|
100
|
+
end.not_to raise_error
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "ACCEPTS a block with a splat positional arg" do
|
|
104
|
+
another_host = Module.new do
|
|
105
|
+
extend Ruact::ServerAction
|
|
106
|
+
|
|
107
|
+
def self.name
|
|
108
|
+
"SplatHost"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
expect do
|
|
112
|
+
another_host.module_eval { ruact_action(:create_post) { |*_args| nil } }
|
|
113
|
+
end.not_to raise_error
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "ACCEPTS a block with optional keyword args" do
|
|
117
|
+
another_host = Module.new do
|
|
118
|
+
extend Ruact::ServerAction
|
|
119
|
+
|
|
120
|
+
def self.name
|
|
121
|
+
"OptKwHost"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
expect do
|
|
125
|
+
another_host.module_eval { ruact_action(:create_post) { |_p, key: nil| key } }
|
|
126
|
+
end.not_to raise_error
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "raises Ruact::ConfigurationError for a bad naming-bridge symbol (:Create_Post)" do
|
|
130
|
+
expect do
|
|
131
|
+
host.module_eval { ruact_action(:Create_Post) { |_p| nil } }
|
|
132
|
+
end.to raise_error(Ruact::ConfigurationError) do |error|
|
|
133
|
+
expect(error.message).to include(":Create_Post")
|
|
134
|
+
expect(error.message).to include("GuardRailModule")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "raises Ruact::ConfigurationError for a JS-reserved-word target (:class → \"class\")" do
|
|
139
|
+
expect do
|
|
140
|
+
host.module_eval { ruact_action(:class) { |_p| nil } }
|
|
141
|
+
end.to raise_error(Ruact::ConfigurationError, /JS reserved word/)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "raises Ruact::ConfigurationError for a ruact-runtime-reserved target (:revalidate)" do
|
|
145
|
+
expect do
|
|
146
|
+
host.module_eval { ruact_action(:revalidate) { |_p| nil } }
|
|
147
|
+
end.to raise_error(Ruact::ConfigurationError) do |error|
|
|
148
|
+
expect(error.message).to include("revalidate")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "raises Ruact::ConfigurationError for the second standalone host declaring the same symbol" do
|
|
153
|
+
host.module_eval { ruact_action(:dup_symbol) { |_p| nil } }
|
|
154
|
+
second_host = Module.new do
|
|
155
|
+
extend Ruact::ServerAction
|
|
156
|
+
|
|
157
|
+
def self.name
|
|
158
|
+
"SecondHostModule"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
expect do
|
|
162
|
+
second_host.module_eval { ruact_action(:dup_symbol) { |_p| nil } }
|
|
163
|
+
end.to raise_error(Ruact::ConfigurationError) do |error|
|
|
164
|
+
expect(error.message).to include(":dup_symbol")
|
|
165
|
+
expect(error.message).to include("GuardRailModule")
|
|
166
|
+
expect(error.message).to include("SecondHostModule")
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "does NOT have a FRAMEWORK_RESERVED_METHODS check — standalone modules can use " \
|
|
171
|
+
"names that would clobber an ActionController method (e.g., :params), since the " \
|
|
172
|
+
"module has no ActionController surface" do
|
|
173
|
+
expect do
|
|
174
|
+
Module.new do
|
|
175
|
+
extend Ruact::ServerAction
|
|
176
|
+
|
|
177
|
+
def self.name
|
|
178
|
+
"FrameworkResvHost"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
ruact_action(:params) { |_p| nil }
|
|
182
|
+
end
|
|
183
|
+
end.not_to raise_error
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "Story 8.3 review R4 — rejects Class hosts at first ruact_action call with a " \
|
|
187
|
+
"documented Ruact::ConfigurationError pointing the dev to `include Ruact::Controller`" do
|
|
188
|
+
klass = Class.new do
|
|
189
|
+
extend Ruact::ServerAction
|
|
190
|
+
|
|
191
|
+
def self.name
|
|
192
|
+
"WronglyExtendedClass"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
expect do
|
|
197
|
+
klass.class_eval { ruact_action(:bad_host) { |_p| nil } }
|
|
198
|
+
end.to raise_error(Ruact::ConfigurationError) do |error|
|
|
199
|
+
expect(error.message).to include("WronglyExtendedClass")
|
|
200
|
+
expect(error.message).to include("standalone HOST MODULES")
|
|
201
|
+
expect(error.message).to include("include Ruact::Controller")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "does NOT install a method_added hook — a later `def` on the host module does not " \
|
|
206
|
+
"raise (standalone modules don't define action methods at all)" do
|
|
207
|
+
expect do
|
|
208
|
+
Module.new do
|
|
209
|
+
extend Ruact::ServerAction
|
|
210
|
+
|
|
211
|
+
def self.name
|
|
212
|
+
"MethodAddedHost"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
ruact_action(:registered_action) { |_p| nil }
|
|
216
|
+
|
|
217
|
+
# A later method definition on the module would not trigger anything
|
|
218
|
+
# — the registry holds the block; the module surface is irrelevant.
|
|
219
|
+
define_method(:registered_action) { :unrelated_def }
|
|
220
|
+
end
|
|
221
|
+
end.not_to raise_error
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Story 8.3 — `Ruact::ServerFunctions::StandaloneContext`: AC3 surface
|
|
4
|
+
# (exposes params/request/session/cookies/headers; render/redirect_to/head
|
|
5
|
+
# raise NoMethodError; current_user memoizes + falls back to env key +
|
|
6
|
+
# raises CurrentUserNotConfiguredError when neither path produces a value).
|
|
7
|
+
|
|
8
|
+
require "spec_helper"
|
|
9
|
+
require "action_controller"
|
|
10
|
+
require "action_dispatch"
|
|
11
|
+
|
|
12
|
+
module Ruact
|
|
13
|
+
module ServerFunctions
|
|
14
|
+
RSpec.describe StandaloneContext, :story_8_3 do
|
|
15
|
+
let(:env) { {} }
|
|
16
|
+
let(:request) do
|
|
17
|
+
ActionDispatch::Request.new(env.merge(
|
|
18
|
+
"REQUEST_METHOD" => "POST",
|
|
19
|
+
"rack.input" => StringIO.new("")
|
|
20
|
+
))
|
|
21
|
+
end
|
|
22
|
+
let(:params) { ActionController::Parameters.new("title" => "Hello") }
|
|
23
|
+
let(:context) { described_class.new(params: params, request: request) }
|
|
24
|
+
|
|
25
|
+
describe "exposed surface (AC3)" do
|
|
26
|
+
it "exposes params (the action-call args, as ActionController::Parameters)" do
|
|
27
|
+
expect(context.params).to be_a(ActionController::Parameters)
|
|
28
|
+
expect(context.params[:title]).to eq("Hello")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "exposes the live request" do
|
|
32
|
+
expect(context.request).to be(request)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "exposes headers via request.headers" do
|
|
36
|
+
expect(context.headers).to be(request.headers)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "blocked surface — render / redirect_to / head (AC3)" do
|
|
41
|
+
it "render raises NoMethodError with the documented hint" do
|
|
42
|
+
expect { context.render(json: { ok: true }) }
|
|
43
|
+
.to raise_error(NoMethodError, /does not expose `render`/)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "redirect_to raises NoMethodError with the documented hint" do
|
|
47
|
+
expect { context.redirect_to("/login") }
|
|
48
|
+
.to raise_error(NoMethodError, /does not expose `redirect_to`/)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "head raises NoMethodError with the documented hint" do
|
|
52
|
+
expect { context.head(:no_content) }
|
|
53
|
+
.to raise_error(NoMethodError, /does not expose `head`/)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "current_user resolver path (AC3)" do
|
|
58
|
+
around do |example|
|
|
59
|
+
Ruact.instance_variable_set(:@config, nil)
|
|
60
|
+
Ruact.instance_variable_set(:@configured_at_least_once, false)
|
|
61
|
+
example.run
|
|
62
|
+
ensure
|
|
63
|
+
Ruact.instance_variable_set(:@config, nil)
|
|
64
|
+
Ruact.instance_variable_set(:@configured_at_least_once, false)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
before do
|
|
68
|
+
# Silence the warn-on-reconfigure noise.
|
|
69
|
+
allow(Rails).to receive(:logger).and_return(Logger.new(IO::NULL))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "raises Ruact::CurrentUserNotConfiguredError when no resolver is configured AND no env key is set" do
|
|
73
|
+
expect { context.current_user }.to raise_error(Ruact::CurrentUserNotConfiguredError) do |err|
|
|
74
|
+
expect(err.message).to include("Ruact.current_user requires Ruact.config.current_user_resolver")
|
|
75
|
+
expect(err.message).to include("Devise")
|
|
76
|
+
expect(err.message).to include("hand-rolled session")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "returns the configured resolver's value, passing request.env to the lambda" do
|
|
81
|
+
user = Struct.new(:id).new(42)
|
|
82
|
+
Ruact.configure do |c|
|
|
83
|
+
c.current_user_resolver = lambda { |env_arg|
|
|
84
|
+
expect(env_arg).to be(request.env)
|
|
85
|
+
user
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
expect(context.current_user).to be(user)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "memoizes current_user across multiple calls (resolver invoked once)" do
|
|
92
|
+
call_count = 0
|
|
93
|
+
Ruact.configure do |c|
|
|
94
|
+
c.current_user_resolver = lambda { |_env|
|
|
95
|
+
call_count += 1
|
|
96
|
+
:user
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
context.current_user
|
|
100
|
+
context.current_user
|
|
101
|
+
context.current_user
|
|
102
|
+
expect(call_count).to eq(1)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "prefers an upstream-set request.env['ruact.current_user'] over the resolver" do
|
|
106
|
+
env["ruact.current_user"] = :upstream_user
|
|
107
|
+
resolver_called = false
|
|
108
|
+
Ruact.configure do |c|
|
|
109
|
+
c.current_user_resolver = lambda { |_env|
|
|
110
|
+
resolver_called = true
|
|
111
|
+
:resolver_user
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
expect(context.current_user).to eq(:upstream_user)
|
|
115
|
+
expect(resolver_called).to be(false)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "falls back to the resolver when the env key is absent (not just nil)" do
|
|
119
|
+
# Pitfall: env.key? differs from env.fetch — a `nil` value should still
|
|
120
|
+
# prefer the env path. Verify by setting nil explicitly.
|
|
121
|
+
env["ruact.current_user"] = nil
|
|
122
|
+
Ruact.configure do |c|
|
|
123
|
+
c.current_user_resolver = ->(_env) { :resolver_user }
|
|
124
|
+
end
|
|
125
|
+
expect(context.current_user).to be_nil
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe "__ruact_current_user_read? (Pitfall #4 dev warning flag)" do
|
|
130
|
+
it "is false when the block never reads current_user" do
|
|
131
|
+
expect(context.__ruact_current_user_read?).to be(false)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "flips to true when the block reads current_user" do
|
|
135
|
+
Ruact.configure { |c| c.current_user_resolver = ->(_env) { :u } }
|
|
136
|
+
context.current_user
|
|
137
|
+
expect(context.__ruact_current_user_read?).to be(true)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|