inline_forms 8.1.53 → 8.1.56

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 698cbe91c323804016404aed86eda2e03d171b2a5df33ab775d3cca68f3d4341
4
- data.tar.gz: 7ea3231464409d606c023f4b34aeeb2212d293abeaefc7a876e5ec488f6dcc8b
3
+ metadata.gz: 7ecd64e62be7265a5989cc6012533cdac80dbd62f82ab8b009c17bd4c12343dd
4
+ data.tar.gz: fb677f7996a52a2a8d571f65264f22225a9ff9ed8c451657a2ac59c268d396d5
5
5
  SHA512:
6
- metadata.gz: b2c7081ad209dcb79cc50ffa9cd61eaa920a9e44b609d4460a70df0aecfc65532ade283e4335836d0f824714e76107aa888931992d52f777e96868a28336ca4b
7
- data.tar.gz: 0b7dc0db8f243a6d06467fa353d423252a97c8c33b14940f24264c99aa3d851253d8580a78e2ce0265d856925ae06753da1cb1d8b793c6716014262b03e093eb
6
+ metadata.gz: a5d4ed8bbb6569904df70ebbbd30b57c4847bb478088dfe70419363c58caf11d19dc6dfb5c67da2fbdaad8a85555c5b74b68847d19484f272d7c5fcbebcc30cf
7
+ data.tar.gz: a5b41fd5b309e7fd73bec6c652609f775493c1b3efdf977d3aed1503c6e6656d4e8d7e695bac99ca751b914bb2a47be64bdb244233820253d2426e87240515ee
data/CHANGELOG.md CHANGED
@@ -4,6 +4,61 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [8.1.56] - 2026-10-05
8
+
9
+ ### Fixed
10
+
11
+ - **HTML requests the engine refuses get an explicit answer instead of falling through to implicit rendering.** Several `InlineFormsController` actions registered `format.html` only behind a guard, or registered it with a block that rendered nothing on the refused path. Rails then rendered implicitly: `ActionController::MissingExactTemplate` (a 500 for browser requests), `UnknownFormat` (406) or a bare 204. That meant error pages and exception-notifier noise. StProject hit it as a 500 on `GET /dagrapportages` by a normal user: `Dagrapportage` is `not_accessible_through_html?` and only exists nested in a Client tab. Now:
12
+ - `index` of a `not_accessible_through_html?` model without `parent_class` → **404**. The resource has no top-level page.
13
+ - `new`, `create`, row `show`, `destroy`, `soft_delete`, `soft_restore` and `revert` without the frame context the gem needs (`update`, plus `parent_class` or a nested row frame for `not_accessible_through_html?` models) → **400**. The request is malformed for this UI rather than aimed at a missing resource, so 400 rather than 404. A guard that does not match the request is a refusal of the request.
14
+ - `destroy` by a user who may not hard-destroy (`destroy_permitted?`, i.e. not superadmin) → **403**, the same answer `revert` already gave. It used to render nothing.
15
+ - `create`, `destroy`, `soft_delete`, `soft_restore` and `revert` now refuse **before writing**. Before, `create` saved the record, and `destroy`/`revert` changed data, and only then failed to respond.
16
+ - The refusals happen after `load_and_authorize_resource` / `authorize!`, so a host's CanCan `check_authorization` stays satisfied. The working paths are unchanged, including the turbo_stream responses (open-after-create, revert), nested lists, forms and rows, and accessible models' lists and rows.
17
+ - **File-slot actions under `check_authorization`.** `file_slot_request_permitted?` answered 400 for an unlisted attribute before any `authorize!`, so a host with CanCan's `check_authorization` raised `CanCan::AuthorizationNotPerformed` (500) instead. It now authorizes the record first, then the attribute.
18
+
19
+ ### Tests
20
+
21
+ - `test/integration/html_refusal_test.rb`, using new dummy model `DailyReport` (a `not_accessible_through_html?` child of `Machine`):
22
+ - top-level index → 404; show, new and revert without a nested frame → 400; create → 400 with nothing created; destroy → 400 with the record kept;
23
+ - nested list, new, create, row, destroy and revert still work;
24
+ - an accessible model's list and row still render, and its new/create without `update` → 400;
25
+ - a non-superadmin destroy → 403;
26
+ - a refused file-slot request still counts as authorized.
27
+ - Every refusal also asserts that authorization ran.
28
+
29
+ ### Lockstep
30
+
31
+ - validation_hints 8.1.56, inline_forms_installer 8.1.56, inline_forms_schema_edit 8.1.56.
32
+
33
+ ## [8.1.55] - 2026-10-04
34
+
35
+ ### Fixed
36
+
37
+ - **Published gems were unreadable for other users.** RubyGems packages each file with its on-disk mode, and `gem install` restores it. The release checkout is group-only (`rw-rw----`), so every file in the published 8.1.53 gems was `0660`. After `gem install` only the installing user and group could read them: for anyone else `require "inline_forms"` failed silently (Bundler swallows that LoadError), so the app died at boot with `uninitialized constant InlineForms`. A Docker image that installs gems as root and runs as another user could not boot at all. Gem builds now package every file as `0644`, or `0755` when the owner may execute it, whatever the checkout's modes: `InlineFormsGemFiles` wraps `Gem::Package#add_files`, and all three gemspecs in this repo load it, so `gem build`, `rake build` and the CI full gate are all covered. validation_hints 8.1.55 has the same fix. 8.1.53 cannot be republished (RubyGems never accepts a version twice), so apps on 8.1.53 should move to 8.1.55.
38
+
39
+ ### Tests
40
+
41
+ - Gem: `test/gem_package_modes_test.rb`: group-only files on disk are packaged `0644`/`0755`, and the inline_forms, inline_forms_installer and inline_forms_schema_edit gemspecs build with every file world-readable.
42
+
43
+ ### Lockstep
44
+
45
+ - validation_hints 8.1.55, inline_forms_installer 8.1.55, inline_forms_schema_edit 8.1.55.
46
+
47
+ ## [8.1.54] - 2026-10-04
48
+
49
+ ### Fixed
50
+
51
+ - **"Forgot your password?" and the password reset page raised.** Devise 5 removed `devise_error_messages!`, and the gem's `devise/passwords/new` and `devise/passwords/edit` still called it, so both pages failed with `undefined method 'devise_error_messages!'` in every generated app. They now render Devise's own `devise/shared/error_messages` partial (`render "devise/shared/error_messages", resource: resource`).
52
+ - **`devise/shared/_header_and_errors`** (not rendered by the gem's own views; kept for host apps that render it). It looked up a translation named after the app (`t application_name`, which renders a translation_missing span), called the removed `devise_error_messages!`, and looked up `inline_forms.devise.` when `@default_message` was unset. It now shows `t('application_name', default: application_name)` like the `inline_forms` layout's title, Devise's error list when the resource has errors, and the `inline_forms.devise.<@default_message>` text only when `@default_message` is set.
53
+
54
+ ### Tests
55
+
56
+ - Example app: `example_app_devise_passwords_test.rb`: the forgot-password and reset-password pages render, an unknown email re-renders the form with Devise's error list, and `_header_and_errors` shows the app name and the resource errors. The engine's dummy app has no Devise, so these tests live in the example app only.
57
+
58
+ ### Lockstep
59
+
60
+ - validation_hints 8.1.54, inline_forms_installer 8.1.54, inline_forms_schema_edit 8.1.54.
61
+
7
62
  ## [8.1.53] - 2026-10-04
8
63
 
9
64
  ### Fixed
@@ -136,10 +136,16 @@ module FileSlotsConcern
136
136
  # Validates the attribute (declared slot shown as :simple_file_field in the
137
137
  # attribute list), then authorizes +action+ on it. Renders and returns false
138
138
  # when refused.
139
+ #
140
+ # The record is authorized first (these actions skip
141
+ # load_and_authorize_resource): a 400 for an unlisted attribute must still
142
+ # count as an authorization check, or a host with CanCan's
143
+ # +check_authorization+ raises AuthorizationNotPerformed (500) instead.
139
144
  def file_slot_request_permitted?(action)
140
145
  @attribute = params[:attribute].to_s
141
146
  @form_element = "simple_file_field"
142
147
  @update_span = params[:update]
148
+ authorize!(action, @object) if cancan_enabled?
143
149
  unless inline_forms_file_slot_listed?(@object, @attribute)
144
150
  head :bad_request
145
151
  return false
@@ -84,6 +84,11 @@ class InlineFormsController < ApplicationController
84
84
  format.html do
85
85
  if @parent_class.present?
86
86
  render_nested_associated_list_html
87
+ else
88
+ # No top-level page for this resource. An explicit answer: an
89
+ # empty block would fall through to implicit rendering and raise
90
+ # MissingExactTemplate (500).
91
+ head :not_found
87
92
  end
88
93
  end
89
94
  else
@@ -116,7 +121,7 @@ class InlineFormsController < ApplicationController
116
121
 
117
122
  @object.inline_forms_attribute_list = @inline_forms_attribute_list if @inline_forms_attribute_list
118
123
  respond_to do |format|
119
- format.html { render_turbo_new } if html_list_flow_allowed?
124
+ format.html { html_list_flow_allowed? ? render_turbo_new : head(:bad_request) }
120
125
  end
121
126
  end
122
127
 
@@ -139,6 +144,11 @@ class InlineFormsController < ApplicationController
139
144
  def create
140
145
  @object ||= @Klass.new
141
146
  @update_span = params[:update]
147
+ @parent_class = params[:parent_class]
148
+ # Refuse before anything is written: the responses below only exist for
149
+ # a request from a list or nested-list frame.
150
+ return head :bad_request unless html_list_flow_allowed?
151
+
142
152
  attributes = @inline_forms_attribute_list || @object.inline_forms_attribute_list
143
153
  attributes.each do | attribute, form_element |
144
154
  # Skip a row whose column is not migrated yet (model edited, migration
@@ -149,7 +159,6 @@ class InlineFormsController < ApplicationController
149
159
  InlineForms.assert_plain_text_column!(object: @object, attribute: attribute, form_element: form_element)
150
160
  send("#{form_element}_update", @object, attribute) unless form_element == :associated || (cancan_enabled? && cannot?(:read, @object, attribute))
151
161
  end
152
- @parent_class = params[:parent_class]
153
162
  @parent_id = params[:parent_id]
154
163
  # See #index for the order/search/parent-fk decomposition.
155
164
  fk_conditions = nil
@@ -171,19 +180,17 @@ class InlineFormsController < ApplicationController
171
180
  created_object = @object
172
181
  @object = nil
173
182
  respond_to do |format|
174
- if html_list_flow_allowed?
175
- if open_created_row?(created_object, attributes)
176
- format.turbo_stream { render_created_row_open_streams(created_object) }
177
- end
178
- format.html { render_list_frame_after_save }
183
+ if open_created_row?(created_object, attributes)
184
+ format.turbo_stream { render_created_row_open_streams(created_object) }
179
185
  end
186
+ format.html { render_list_frame_after_save }
180
187
  end
181
188
  else
182
189
  flash.now[:header] = [ "Kan #{@object.class.to_s.underscore} niet aanmaken." ]
183
190
  flash.now[:error] = @object.errors.to_a
184
191
  respond_to do |format|
185
192
  @object.inline_forms_attribute_list = attributes
186
- format.html { render_turbo_new } if html_list_flow_allowed?
193
+ format.html { render_turbo_new }
187
194
  end
188
195
  end
189
196
  end
@@ -257,10 +264,10 @@ class InlineFormsController < ApplicationController
257
264
  if @attribute.nil?
258
265
  respond_to do |format|
259
266
  @attributes = @object.inline_forms_attribute_list
260
- if close
261
- format.html { render_row_turbo(:close) } if row_html_turbo_allowed?
262
- else
263
- format.html { render_row_turbo(:show) } if row_html_turbo_allowed?
267
+ format.html do
268
+ next head(:bad_request) unless row_html_turbo_allowed?
269
+
270
+ render_row_turbo(close ? :close : :show)
264
271
  end
265
272
  end
266
273
  else
@@ -274,9 +281,11 @@ class InlineFormsController < ApplicationController
274
281
  def soft_delete
275
282
  @update_span = params[:update]
276
283
  @object = referenced_object
284
+ return head :bad_request unless row_html_turbo_allowed?
285
+
277
286
  @object.soft_delete(current_user)
278
287
  respond_to do |format|
279
- format.html { render_row_turbo(:close) } if row_html_turbo_allowed?
288
+ format.html { render_row_turbo(:close) }
280
289
  end
281
290
  end
282
291
 
@@ -284,9 +293,11 @@ class InlineFormsController < ApplicationController
284
293
  def soft_restore
285
294
  @update_span = params[:update]
286
295
  @object = referenced_object
296
+ return head :bad_request unless row_html_turbo_allowed?
297
+
287
298
  @object.soft_restore
288
299
  respond_to do |format|
289
- format.html { render_row_turbo(:close) } if row_html_turbo_allowed?
300
+ format.html { render_row_turbo(:close) }
290
301
  end
291
302
  end
292
303
 
@@ -294,14 +305,17 @@ class InlineFormsController < ApplicationController
294
305
  def destroy
295
306
  @update_span = params[:update]
296
307
  @object = referenced_object
297
- if destroy_permitted?
298
- @object.destroy
299
- # Capture after destroy: `.last` before destroy was the latest *update*
300
- # (e.g. a plain_text_area edit), so undo reified the pre-edit state.
301
- @undo_version = @object.versions.last
302
- respond_to do |format|
303
- format.html { render_row_turbo_destroyed } if row_html_turbo_allowed?
304
- end
308
+ # Both refusals answer before anything is destroyed (they used to render
309
+ # nothing and fall through to implicit rendering: a 500).
310
+ return head :forbidden unless destroy_permitted?
311
+ return head :bad_request unless row_html_turbo_allowed?
312
+
313
+ @object.destroy
314
+ # Capture after destroy: `.last` before destroy was the latest *update*
315
+ # (e.g. a plain_text_area edit), so undo reified the pre-edit state.
316
+ @undo_version = @object.versions.last
317
+ respond_to do |format|
318
+ format.html { render_row_turbo_destroyed }
305
319
  end
306
320
  end
307
321
 
@@ -327,6 +341,7 @@ class InlineFormsController < ApplicationController
327
341
  # has the Devise+roles system, otherwise no extra gate beyond CanCan.
328
342
  return head :forbidden unless destroy_permitted?
329
343
  return head :not_found unless @parent
344
+ return head :bad_request unless row_html_turbo_allowed?
330
345
 
331
346
  @object = reify_for_revert(@version)
332
347
  # PaperTrail::Version#reify returns nil for `create` events because
@@ -354,8 +369,7 @@ class InlineFormsController < ApplicationController
354
369
  @parent = item || @parent
355
370
  return head :not_found unless @parent
356
371
  end
357
- return render_revert_response if row_html_turbo_allowed?
358
- return
372
+ return render_revert_response
359
373
  end
360
374
  if defined?(ActionText::RichText) && @object.is_a?(ActionText::RichText)
361
375
  @rich_text_record = @object
@@ -374,7 +388,7 @@ class InlineFormsController < ApplicationController
374
388
  end
375
389
  @parent.reload
376
390
  end
377
- render_revert_response if row_html_turbo_allowed?
391
+ render_revert_response
378
392
  end
379
393
 
380
394
  private
@@ -18,7 +18,7 @@
18
18
  }
19
19
  ) do |f| %>
20
20
 
21
- <%= devise_error_messages! %>
21
+ <%= render "devise/shared/error_messages", resource: resource %>
22
22
  <%= f.hidden_field :reset_password_token %>
23
23
 
24
24
  <div class="row">
@@ -18,7 +18,7 @@
18
18
  }
19
19
  ) do |f| %>
20
20
 
21
- <%= devise_error_messages! %>
21
+ <%= render "devise/shared/error_messages", resource: resource %>
22
22
 
23
23
  <div class="row">
24
24
  <div class="columns">
@@ -1,4 +1,11 @@
1
- <div id="large_title"><%= t application_name -%></div>
1
+ <%# Unused by the gem's own devise views; kept for host apps that render it. %>
2
+ <%# Same title source as layouts/inline_forms; Devise 5 dropped %>
3
+ <%# devise_error_messages!, so errors use Devise's own error_messages partial. %>
4
+ <div id="large_title"><%= t('application_name', default: application_name) %></div>
2
5
  <div id="flash_message">
3
- <%= devise_error_messages!.empty? ? t("inline_forms.devise.#{@default_message}") : devise_error_messages! -%>
6
+ <% if resource.errors.any? %>
7
+ <%= render "devise/shared/error_messages", resource: resource %>
8
+ <% elsif @default_message.present? %>
9
+ <%= t("inline_forms.devise.#{@default_message}") %>
10
+ <% end %>
4
11
  </div>
@@ -1,5 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
+ require "delegate"
4
+ require "rubygems/package"
5
+
3
6
  module InlineFormsGemFiles
4
7
  INSTALLER_FILE_PREFIXES = %w[
5
8
  bin/inline_forms
@@ -24,8 +27,9 @@ module InlineFormsGemFiles
24
27
  # the token leaked into `gem build` on the release machine. Exclude it hard,
25
28
  # independent of any ignore configuration.
26
29
  #
27
- # Same for local Claude Code files (untracked, per checkout): .claude/
28
- # (launch.json, settings.local.json) and CLAUDE.md.
30
+ # Same for Claude Code files: .claude/ (untracked, per checkout:
31
+ # launch.json, settings.local.json, worktrees) and CLAUDE.md (committed so
32
+ # worktrees see it, but repo instructions, not gem content).
29
33
  EXCLUDED_FILE_PREFIXES = %w[
30
34
  stuff/
31
35
  .claude/
@@ -68,4 +72,33 @@ module InlineFormsGemFiles
68
72
  include_installer ? !installer_file : installer_file
69
73
  end
70
74
  end
75
+
76
+ # Mode a file gets inside the .gem: 0755 if the owner may execute it,
77
+ # else 0644. RubyGems packages each file's on-disk mode (File.lstat) and
78
+ # `gem install` restores it, and this checkout is group-only (rw-rw----),
79
+ # so 8.1.53 shipped every file as 0660: only the installing user and group
80
+ # could read the installed gem, `require "inline_forms"` failed silently
81
+ # for anyone else (Bundler swallows the LoadError), and a Docker image that
82
+ # installs as root and runs as another user could not boot.
83
+ def packaged_mode(mode)
84
+ (mode & 0o100).zero? ? 0o644 : 0o755
85
+ end
86
+
87
+ # Wraps the tar writer Gem::Package#add_files writes to, so every file
88
+ # goes in with packaged_mode instead of its on-disk mode.
89
+ class ReadableTar < SimpleDelegator
90
+ def add_file_simple(name, mode, size, &block)
91
+ __getobj__.add_file_simple(name, InlineFormsGemFiles.packaged_mode(mode), size, &block)
92
+ end
93
+ end
94
+
95
+ module ReadablePackage
96
+ def add_files(tar)
97
+ super(ReadableTar.new(tar))
98
+ end
99
+ end
71
100
  end
101
+
102
+ # Loaded by every gemspec in this repo, so `gem build`, `rake build` and the
103
+ # CI full gate all package world-readable files whatever the checkout's modes.
104
+ Gem::Package.prepend(InlineFormsGemFiles::ReadablePackage) unless Gem::Package <= InlineFormsGemFiles::ReadablePackage
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  module InlineForms
4
- VERSION = "8.1.53"
4
+ VERSION = "8.1.56"
5
5
  end
@@ -5,10 +5,13 @@ class ApplicationController < ActionController::Base
5
5
  # create/new HTML responses) calls `current_user.name` and
6
6
  # `current_user.role?`, so a nil current_user cannot render it. The stub is
7
7
  # superadmin so `destroy_permitted?` allows hard destroy, matching the
8
- # generated apps' test user.
8
+ # generated apps' test user. A test can switch that off with
9
+ # dummy_user_superadmin (reset in InlineFormsIntegrationTestCase#setup).
10
+ mattr_accessor :dummy_user_superadmin, default: true
11
+
9
12
  DummyUser = Struct.new(:id, :name) do
10
13
  def role?(role)
11
- role.to_sym == :superadmin
14
+ role.to_sym == :superadmin && ApplicationController.dummy_user_superadmin
12
15
  end
13
16
  end
14
17
 
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DailyReportsController < InlineFormsController
4
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Child of Machine that has no top-level HTML surface: it is only shown
4
+ # nested in its machine (like StProject's Dagrapportage inside a Client tab).
5
+ class DailyReport < ApplicationRecord
6
+ belongs_to :machine, optional: true
7
+
8
+ def self.not_accessible_through_html?
9
+ true
10
+ end
11
+
12
+ def _presentation
13
+ "#{name}"
14
+ end
15
+
16
+ def inline_forms_attribute_list
17
+ @inline_forms_attribute_list ||= [
18
+ [ :name, :text_field ]
19
+ ]
20
+ end
21
+ end
@@ -5,6 +5,7 @@
5
5
  # open-after-create flow (InlineFormsController#render_created_row_open_streams).
6
6
  class Machine < ApplicationRecord
7
7
  has_many :parts
8
+ has_many :daily_reports
8
9
 
9
10
  validates :name, presence: true
10
11
 
@@ -20,6 +20,11 @@ Rails.application.routes.draw do
20
20
  get "list_versions", on: :member
21
21
  end
22
22
 
23
+ resources :daily_reports do
24
+ post "revert", on: :member
25
+ get "list_versions", on: :member
26
+ end
27
+
23
28
  resources :gizmos do
24
29
  post "revert", on: :member
25
30
  get "list_versions", on: :member
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "minitest/autorun"
6
+ require "tmpdir"
7
+ require "zlib"
8
+ require "inline_forms/gem_files"
9
+
10
+ # 8.1.53 shipped every file as 0660 (the checkout's group-only modes), so an
11
+ # installed gem was unreadable for anyone outside the installing user and
12
+ # group. Gem builds now package 0644 / 0755 whatever the on-disk modes.
13
+ class GemPackageModesTest < Minitest::Test
14
+ REPO_ROOT = File.expand_path("..", __dir__)
15
+
16
+ def packaged_modes(gem_path)
17
+ modes = {}
18
+ File.open(gem_path, "rb") do |io|
19
+ Gem::Package::TarReader.new(io).each do |entry|
20
+ next unless entry.full_name == "data.tar.gz"
21
+
22
+ Gem::Package::TarReader.new(Zlib::GzipReader.new(entry)).each do |file|
23
+ modes[file.full_name] = file.header.mode & 0o777
24
+ end
25
+ end
26
+ end
27
+ modes
28
+ end
29
+
30
+ def build(spec, dir, gem_dir)
31
+ gem_path = File.join(gem_dir, "#{spec.full_name}.gem")
32
+ Dir.chdir(dir) do
33
+ package = Gem::Package.new(gem_path)
34
+ package.spec = spec
35
+ Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) { package.build(true) }
36
+ end
37
+ gem_path
38
+ end
39
+
40
+ def test_packaged_mode_keeps_only_the_owner_execute_bit
41
+ assert_equal 0o644, InlineFormsGemFiles.packaged_mode(0o100660)
42
+ assert_equal 0o644, InlineFormsGemFiles.packaged_mode(0o100600)
43
+ assert_equal 0o755, InlineFormsGemFiles.packaged_mode(0o100770)
44
+ assert_equal 0o755, InlineFormsGemFiles.packaged_mode(0o100700)
45
+ end
46
+
47
+ def test_group_only_files_are_packaged_world_readable
48
+ Dir.mktmpdir do |src|
49
+ Dir.mktmpdir do |out|
50
+ File.write(File.join(src, "plain.rb"), "# plain\n")
51
+ File.write(File.join(src, "tool"), "#!/bin/sh\n")
52
+ File.chmod(0o660, File.join(src, "plain.rb"))
53
+ File.chmod(0o700, File.join(src, "tool"))
54
+
55
+ spec = Gem::Specification.new do |s|
56
+ s.name = "modes_probe"
57
+ s.version = "0.0.1"
58
+ s.summary = "probe"
59
+ s.authors = [ "probe" ]
60
+ s.files = %w[plain.rb tool]
61
+ end
62
+
63
+ modes = packaged_modes(build(spec, src, out))
64
+
65
+ assert_equal({ "plain.rb" => 0o644, "tool" => 0o755 }, modes)
66
+ end
67
+ end
68
+ end
69
+
70
+ def test_every_gemspec_in_the_repo_packages_world_readable_files
71
+ %w[inline_forms.gemspec inline_forms_installer.gemspec
72
+ inline_forms_schema_edit/inline_forms_schema_edit.gemspec].each do |gemspec|
73
+ path = File.join(REPO_ROOT, gemspec)
74
+ spec = Gem::Specification.load(path)
75
+ refute_nil spec, gemspec
76
+
77
+ Dir.mktmpdir do |out|
78
+ modes = packaged_modes(build(spec, File.dirname(path), out))
79
+
80
+ refute_empty modes, gemspec
81
+ unreadable = modes.reject { |_, mode| (mode & 0o444) == 0o444 }
82
+ assert_empty unreadable, "#{gemspec}: not world-readable in the .gem"
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration_test_helper"
4
+
5
+ # Every HTML request the engine decides not to serve gets an explicit answer:
6
+ # a top-level list of a not_accessible_through_html? model is a 404, a
7
+ # new/create/show/destroy/revert without the frame context the gem needs is a
8
+ # 400. Before, the format.html block rendered nothing (or was not registered),
9
+ # so Rails fell through to implicit rendering: ActionController::
10
+ # MissingExactTemplate (500; StProject's GET /dagrapportages) or UnknownFormat
11
+ # (406). create, destroy and revert also refuse before writing anything.
12
+ #
13
+ # DailyReport is the not_accessible_through_html? child of Machine; Machine is
14
+ # a normal (accessible) model.
15
+ class HtmlRefusalTest < InlineFormsIntegrationTestCase
16
+ setup do
17
+ @machine = Machine.create!(name: "Press")
18
+ @report = DailyReport.create!(name: "Monday", machine: @machine)
19
+ @list_frame = "machine_#{@machine.id}_daily_reports"
20
+ @row_frame = "machine_#{@machine.id}_daily_report_#{@report.id}"
21
+ end
22
+
23
+ def nested_params(update = @list_frame)
24
+ { update: update, parent_class: "Machine", parent_id: @machine.id }
25
+ end
26
+
27
+ def stream_headers(frame_id)
28
+ frame_headers(frame_id).merge("Accept" => "text/vnd.turbo-stream.html")
29
+ end
30
+
31
+ # The refusal ran after authorization, so CanCan's check_authorization (an
32
+ # after_action in host apps) is satisfied.
33
+ def assert_refused(status)
34
+ assert_response status
35
+ assert controller.instance_variable_defined?(:@_authorized),
36
+ "the refused request must still count as authorized (check_authorization)"
37
+ end
38
+
39
+ # ---- not_accessible_through_html? model, top level -----------------------
40
+
41
+ test "the top-level list of a not_accessible_through_html? model is a 404" do
42
+ get daily_reports_path
43
+ assert_refused :not_found
44
+ end
45
+
46
+ test "show without a nested row frame is a 400" do
47
+ get daily_report_path(@report)
48
+ assert_refused :bad_request
49
+ end
50
+
51
+ test "new without update and parent_class is a 400" do
52
+ get new_daily_report_path
53
+ assert_refused :bad_request
54
+ end
55
+
56
+ test "create without update and parent_class is a 400 and creates nothing" do
57
+ assert_no_difference -> { DailyReport.count } do
58
+ post daily_reports_path, params: { name: "Tuesday" }
59
+ end
60
+ assert_refused :bad_request
61
+ end
62
+
63
+ test "destroy without a nested row frame is a 400 and keeps the record" do
64
+ delete daily_report_path(@report)
65
+ assert_refused :bad_request
66
+ assert DailyReport.exists?(@report.id)
67
+ end
68
+
69
+ test "revert without a nested row frame is a 400 and changes nothing" do
70
+ @report.update!(name: "Changed")
71
+ version = @report.versions.where(event: "update").last
72
+
73
+ post revert_daily_report_path(version.id, update: "daily_report_#{@report.id}"),
74
+ headers: stream_headers("daily_report_#{@report.id}")
75
+ assert_refused :bad_request
76
+ assert_equal "Changed", @report.reload.name
77
+ end
78
+
79
+ # ---- the nested forms still work (regression guard) ----------------------
80
+
81
+ test "the nested list, new form, create, row and destroy still render" do
82
+ get daily_reports_path(nested_params), headers: frame_headers(@list_frame)
83
+ assert_response :success
84
+ assert_includes response.body, "Monday"
85
+
86
+ get new_daily_report_path(nested_params), headers: frame_headers(@list_frame)
87
+ assert_response :success
88
+
89
+ assert_difference -> { DailyReport.count }, +1 do
90
+ post daily_reports_path(nested_params), params: { name: "Tuesday" }, headers: frame_headers(@list_frame)
91
+ end
92
+ assert_response :success
93
+ assert_equal @machine.id, DailyReport.find_by!(name: "Tuesday").machine_id
94
+
95
+ get daily_report_path(@report, update: @row_frame), headers: frame_headers(@row_frame)
96
+ assert_response :success
97
+ assert_includes response.body, %(<turbo-frame id="#{@row_frame}">)
98
+
99
+ delete daily_report_path(@report, update: @row_frame), headers: frame_headers(@row_frame)
100
+ assert_response :success
101
+ refute DailyReport.exists?(@report.id)
102
+ end
103
+
104
+ test "a nested revert still restores the record" do
105
+ @report.update!(name: "Changed")
106
+ version = @report.versions.where(event: "update").last
107
+
108
+ post revert_daily_report_path(version.id, update: @row_frame), headers: stream_headers(@row_frame)
109
+ assert_response :success
110
+ assert_equal "Monday", @report.reload.name
111
+ end
112
+
113
+ # ---- accessible model ------------------------------------------------------
114
+
115
+ test "an accessible model's list and row still render" do
116
+ get machines_path
117
+ assert_response :success
118
+ assert_includes response.body, "Press"
119
+
120
+ get machine_path(@machine)
121
+ assert_response :success
122
+ end
123
+
124
+ test "an accessible model's new and create without update are a 400; nothing is created" do
125
+ get new_machine_path
126
+ assert_refused :bad_request
127
+
128
+ assert_no_difference -> { Machine.count } do
129
+ post machines_path, params: { name: "Lathe" }
130
+ end
131
+ assert_refused :bad_request
132
+ end
133
+
134
+ test "destroy by a user who may not hard-destroy is a 403 and keeps the record" do
135
+ ApplicationController.dummy_user_superadmin = false
136
+ frame = "machine_#{@machine.id}"
137
+
138
+ delete machine_path(@machine, update: frame), headers: frame_headers(frame)
139
+ assert_refused :forbidden
140
+ assert Machine.exists?(@machine.id)
141
+ end
142
+
143
+ # ---- file slots ------------------------------------------------------------
144
+
145
+ test "a refused file-slot request still counts as authorized" do
146
+ document = Document.create!(title: "Plan")
147
+ frame = "document_#{document.id}_filename"
148
+
149
+ post remove_file_document_path(document, attribute: "title", update: frame), headers: stream_headers(frame)
150
+ assert_refused :bad_request
151
+ end
152
+ end
@@ -49,6 +49,15 @@ ActiveRecord::Schema.define do
49
49
  end
50
50
  add_index :parts, :machine_id
51
51
 
52
+ # DailyReport: a not_accessible_through_html? child of Machine (shaped like
53
+ # StProject's Dagrapportage). See test/integration/html_refusal_test.rb.
54
+ create_table :daily_reports, force: true do |t|
55
+ t.string :name
56
+ t.integer :machine_id
57
+ t.timestamps
58
+ end
59
+ add_index :daily_reports, :machine_id
60
+
52
61
  # Gizmo intentionally has NO `pending_note` column: its attribute_list
53
62
  # references one, modeling the inline_forms_addto pre-migrate window that
54
63
  # InlineForms.attribute_pending_migration? gates. See
@@ -163,9 +172,11 @@ class InlineFormsIntegrationTestCase < ActionDispatch::IntegrationTest
163
172
 
164
173
  setup do
165
174
  Ability.restrictions = nil
175
+ ApplicationController.dummy_user_superadmin = true
166
176
  Widget.delete_all
167
177
  Kind.delete_all
168
178
  Part.delete_all
179
+ DailyReport.delete_all
169
180
  Machine.delete_all
170
181
  Gizmo.delete_all
171
182
  Dossier.delete_all
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inline_forms
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.53
4
+ version: 8.1.56
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -282,6 +282,7 @@ files:
282
282
  - test/archived_form_elements_test.rb
283
283
  - test/attribute_list_test.rb
284
284
  - test/dummy/app/controllers/application_controller.rb
285
+ - test/dummy/app/controllers/daily_reports_controller.rb
285
286
  - test/dummy/app/controllers/documents_controller.rb
286
287
  - test/dummy/app/controllers/dossiers_controller.rb
287
288
  - test/dummy/app/controllers/gizmos_controller.rb
@@ -292,6 +293,7 @@ files:
292
293
  - test/dummy/app/helpers/application_helper.rb
293
294
  - test/dummy/app/models/ability.rb
294
295
  - test/dummy/app/models/application_record.rb
296
+ - test/dummy/app/models/daily_report.rb
295
297
  - test/dummy/app/models/document.rb
296
298
  - test/dummy/app/models/dossier.rb
297
299
  - test/dummy/app/models/gizmo.rb
@@ -308,11 +310,13 @@ files:
308
310
  - test/dummy/config/initializers/paper_trail_yaml_safe_load.rb
309
311
  - test/dummy/config/routes.rb
310
312
  - test/form_element_from_callee_test.rb
313
+ - test/gem_package_modes_test.rb
311
314
  - test/inline_edit_polymorphic_path_test.rb
312
315
  - test/inline_forms_addto_generator_test.rb
313
316
  - test/inline_forms_generator_test.rb
314
317
  - test/integration/field_authorization_test.rb
315
318
  - test/integration/file_slots_test.rb
319
+ - test/integration/html_refusal_test.rb
316
320
  - test/integration/inline_forms_crud_test.rb
317
321
  - test/integration/layout_title_test.rb
318
322
  - test/integration/legacy_elements_test.rb
@@ -362,6 +366,7 @@ test_files:
362
366
  - test/archived_form_elements_test.rb
363
367
  - test/attribute_list_test.rb
364
368
  - test/dummy/app/controllers/application_controller.rb
369
+ - test/dummy/app/controllers/daily_reports_controller.rb
365
370
  - test/dummy/app/controllers/documents_controller.rb
366
371
  - test/dummy/app/controllers/dossiers_controller.rb
367
372
  - test/dummy/app/controllers/gizmos_controller.rb
@@ -372,6 +377,7 @@ test_files:
372
377
  - test/dummy/app/helpers/application_helper.rb
373
378
  - test/dummy/app/models/ability.rb
374
379
  - test/dummy/app/models/application_record.rb
380
+ - test/dummy/app/models/daily_report.rb
375
381
  - test/dummy/app/models/document.rb
376
382
  - test/dummy/app/models/dossier.rb
377
383
  - test/dummy/app/models/gizmo.rb
@@ -388,11 +394,13 @@ test_files:
388
394
  - test/dummy/config/initializers/paper_trail_yaml_safe_load.rb
389
395
  - test/dummy/config/routes.rb
390
396
  - test/form_element_from_callee_test.rb
397
+ - test/gem_package_modes_test.rb
391
398
  - test/inline_edit_polymorphic_path_test.rb
392
399
  - test/inline_forms_addto_generator_test.rb
393
400
  - test/inline_forms_generator_test.rb
394
401
  - test/integration/field_authorization_test.rb
395
402
  - test/integration/file_slots_test.rb
403
+ - test/integration/html_refusal_test.rb
396
404
  - test/integration/inline_forms_crud_test.rb
397
405
  - test/integration/layout_title_test.rb
398
406
  - test/integration/legacy_elements_test.rb