inline_forms 8.1.47 → 8.1.53

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/CHANGELOG.md +149 -0
  4. data/Gemfile +3 -0
  5. data/README.rdoc +47 -0
  6. data/app/assets/stylesheets/inline_forms/inline_forms.scss +34 -0
  7. data/app/controllers/concerns/file_slots_concern.rb +211 -0
  8. data/app/controllers/inline_forms/file_trash_controller.rb +115 -0
  9. data/app/controllers/inline_forms_controller.rb +82 -16
  10. data/app/helpers/inline_forms_file_trash_helper.rb +131 -0
  11. data/app/helpers/inline_forms_helper.rb +5 -1
  12. data/app/models/concerns/inline_forms/stored_files.rb +350 -0
  13. data/app/models/inline_forms/current.rb +8 -0
  14. data/app/models/inline_forms/file_trash_sweep.rb +37 -0
  15. data/app/models/inline_forms/trashed_file.rb +205 -0
  16. data/app/views/inline_forms/_file_slot.html.erb +39 -0
  17. data/app/views/inline_forms/_header.html.erb +3 -0
  18. data/app/views/inline_forms/_model_top_bar.html.erb +3 -4
  19. data/app/views/inline_forms/_show.html.erb +6 -0
  20. data/app/views/inline_forms/_versions.html.erb +4 -4
  21. data/app/views/inline_forms/_versions_file_entry.html.erb +36 -0
  22. data/app/views/inline_forms/_versions_list.html.erb +11 -2
  23. data/app/views/inline_forms/file_field_frame.html.erb +22 -0
  24. data/app/views/inline_forms/file_trash/index.html.erb +112 -0
  25. data/app/views/inline_forms/file_trash_panel.html.erb +90 -0
  26. data/app/views/layouts/inline_forms.html.erb +5 -1
  27. data/lib/generators/inline_forms/file_trash_generator.rb +36 -0
  28. data/lib/generators/inline_forms/templates/create_inline_forms_file_trash.rb.erb +41 -0
  29. data/lib/generators/inline_forms_addto_generator.rb +47 -0
  30. data/lib/generators/inline_forms_generator.rb +19 -2
  31. data/lib/generators/templates/model.erb +4 -0
  32. data/lib/inline_forms/form_element_from_callee.rb +25 -0
  33. data/lib/inline_forms/form_elements/simple_file_field_helper.rb +30 -2
  34. data/lib/inline_forms/gem_files.rb +5 -0
  35. data/lib/inline_forms/version.rb +1 -1
  36. data/lib/inline_forms.rb +55 -0
  37. data/lib/locales/inline_forms.en.yml +87 -0
  38. data/lib/locales/inline_forms.nl.yml +87 -3
  39. data/lib/tasks/inline_forms_files.rake +61 -0
  40. data/test/dummy/app/controllers/application_controller.rb +5 -0
  41. data/test/dummy/app/controllers/documents_controller.rb +4 -0
  42. data/test/dummy/app/controllers/dossiers_controller.rb +4 -0
  43. data/test/dummy/app/models/ability.rb +15 -0
  44. data/test/dummy/app/models/document.rb +33 -0
  45. data/test/dummy/app/models/dossier.rb +38 -0
  46. data/test/dummy/config/application.rb +4 -3
  47. data/test/dummy/config/routes.rb +13 -0
  48. data/test/inline_forms_addto_generator_test.rb +47 -0
  49. data/test/inline_forms_generator_test.rb +24 -0
  50. data/test/integration/field_authorization_test.rb +131 -0
  51. data/test/integration/file_slots_test.rb +574 -0
  52. data/test/integration/layout_title_test.rb +56 -0
  53. data/test/integration/model_top_bar_test.rb +9 -0
  54. data/test/integration/revert_skipped_columns_test.rb +59 -0
  55. data/test/integration_test_helper.rb +62 -0
  56. metadata +34 -3
  57. data/.claude/settings.local.json +0 -9
  58. data/CLAUDE.md +0 -68
@@ -0,0 +1,574 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration_test_helper"
4
+
5
+ # Declared file slots (InlineForms::StoredFiles) and the file trash:
6
+ # upload / replace / remove / restore / purge, permissions per action, the
7
+ # per-record panel, versions history, revert interplay, the retention sweep
8
+ # and the global trash page. Dummy model: Document (test/dummy).
9
+ class FileSlotsTest < InlineFormsIntegrationTestCase
10
+ PLAN = "%PDF-1.4 the plan \x00\x01".b
11
+ OTHER = "%PDF-1.4 another file \x02\x03".b
12
+
13
+ setup do
14
+ @doc = Document.create!(title: "Dossier A")
15
+ @frame = "document_#{@doc.id}_filename"
16
+ end
17
+
18
+ def upload(bytes, name = "plan.pdf", type = "application/pdf")
19
+ Rack::Test::UploadedFile.new(StringIO.new(bytes), type, original_filename: name)
20
+ end
21
+
22
+ def stream_headers(frame)
23
+ frame_headers(frame).merge("Accept" => "text/vnd.turbo-stream.html")
24
+ end
25
+
26
+ def put_file(doc, attribute, value)
27
+ frame = "document_#{doc.id}_#{attribute}"
28
+ patch document_path(doc, attribute: attribute, form_element: "simple_file_field", update: frame),
29
+ params: { attribute => value }, headers: frame_headers(frame)
30
+ end
31
+
32
+ def with_file(bytes = PLAN, name = "plan.pdf")
33
+ @doc.update!(filename: upload(bytes, name))
34
+ @doc.reload
35
+ end
36
+
37
+ def trash
38
+ InlineForms::TrashedFile.trash_entries
39
+ end
40
+
41
+ # ---- declaration ------------------------------------------------------
42
+
43
+ test "slot columns are skipped by PaperTrail on the model only" do
44
+ skip_list = Document.paper_trail_options[:skip]
45
+ %w[filename data content_type plan_filename plan_data plan_content_type].each do |column|
46
+ assert_includes skip_list, column
47
+ end
48
+ refute_includes Widget.paper_trail_options[:skip], "data",
49
+ "the inherited ApplicationRecord options must not be mutated"
50
+ end
51
+
52
+ test "default column names follow the attribute" do
53
+ assert_equal({ data: "data", content_type: "content_type" }, Document.inline_forms_file_slot(:filename))
54
+ assert_equal({ data: "plan_data", content_type: "plan_content_type" }, Document.inline_forms_file_slot(:plan_filename))
55
+ end
56
+
57
+ test "a hand-written setter for a declared slot raises outside production" do
58
+ assert_raises(InlineForms::StoredFiles::HandWrittenSetterError) do
59
+ Class.new(ApplicationRecord) do
60
+ self.table_name = "documents"
61
+ include InlineForms::StoredFiles
62
+ inline_forms_file :filename
63
+ def filename=(value); end
64
+ end
65
+ end
66
+ end
67
+
68
+ test "has_paper_trail after the macro still skips the slot columns" do
69
+ klass = Class.new(ActiveRecord::Base) do
70
+ self.table_name = "documents"
71
+ include InlineForms::StoredFiles
72
+ inline_forms_file :filename
73
+ has_paper_trail
74
+ end
75
+ assert_includes klass.paper_trail_options[:skip], "data"
76
+ end
77
+
78
+ # ---- upload / replace ---------------------------------------------------
79
+
80
+ test "uploading into an empty slot stores bytes and logs an upload event" do
81
+ put_file @doc, "filename", upload(PLAN, "my plan (v2).pdf")
82
+
83
+ assert_response :success
84
+ @doc.reload
85
+ assert_equal PLAN, @doc.data
86
+ assert_equal "my_plan__v2_.pdf", @doc.filename
87
+ assert_equal "application/pdf", @doc.content_type
88
+ event = InlineForms::TrashedFile.events.sole
89
+ assert_equal "filename", event.attribute_name
90
+ assert_nil event.data
91
+ assert_equal 1, event.trashed_by_id
92
+ assert_empty trash
93
+ end
94
+
95
+ test "replacing moves the old bytes to the trash" do
96
+ with_file
97
+ put_file @doc, "filename", upload(OTHER, "other.pdf")
98
+
99
+ assert_response :success
100
+ assert_equal OTHER, @doc.reload.data
101
+ entry = trash.sole
102
+ assert_equal "replaced", entry.reason
103
+ assert_equal PLAN, entry.file_data
104
+ assert_equal "plan.pdf", entry.filename
105
+ assert_equal Digest::SHA256.hexdigest(PLAN), entry.checksum
106
+ assert_in_delta 30.days.from_now, entry.purge_after, 5
107
+ end
108
+
109
+ test "re-uploading identical bytes trashes nothing" do
110
+ with_file
111
+ put_file @doc, "filename", upload(PLAN, "plan.pdf")
112
+
113
+ assert_response :success
114
+ assert_empty trash
115
+ end
116
+
117
+ test "replace needs :replace_file; an empty slot only needs :update" do
118
+ Ability.restrictions = -> { cannot :replace_file, Document }
119
+ put_file @doc, "plan_filename", upload(PLAN)
120
+ assert_response :success
121
+
122
+ with_file
123
+ put_file @doc, "filename", upload(OTHER, "other.pdf")
124
+ assert_response :forbidden
125
+ assert_equal PLAN, @doc.reload.data
126
+ assert_empty trash
127
+ end
128
+
129
+ test "edit of an occupied slot needs :replace_file too" do
130
+ with_file
131
+ Ability.restrictions = -> { cannot :replace_file, Document }
132
+ get edit_document_path(@doc, attribute: "filename", form_element: "simple_file_field", update: @frame),
133
+ headers: frame_headers(@frame)
134
+ assert_response :forbidden
135
+ end
136
+
137
+ test "a crafted string for a slot is refused as 'no file chosen'" do
138
+ with_file
139
+ put_file @doc, "filename", "evil.exe"
140
+
141
+ assert_response :success
142
+ assert_includes response.body, "no file chosen"
143
+ @doc.reload
144
+ assert_equal "plan.pdf", @doc.filename
145
+ assert_equal PLAN, @doc.data
146
+ end
147
+
148
+ test "an upload above max_file_size is a validation error" do
149
+ InlineForms.max_file_size = 10
150
+ put_file @doc, "filename", upload(PLAN)
151
+
152
+ assert_includes response.body, "is too large"
153
+ assert_nil @doc.reload.data
154
+ end
155
+
156
+ test "a failed save does not leave a trash row behind" do
157
+ with_file
158
+ Document.where(id: @doc.id).update_all(title: nil) # an unrelated invalid column
159
+ put_file @doc, "filename", upload(OTHER, "other.pdf")
160
+
161
+ assert_includes response.body, "can"
162
+ assert_equal PLAN, @doc.reload.data
163
+ assert_empty trash
164
+ end
165
+
166
+ # ---- show ---------------------------------------------------------------
167
+
168
+ test "a present slot renders download, replace and remove; an empty one the plus link" do
169
+ with_file
170
+ get document_path(@doc, attribute: "filename", form_element: "simple_file_field", update: @frame),
171
+ headers: frame_headers(@frame)
172
+
173
+ assert_response :success
174
+ assert_includes response.body, download_file_document_path(@doc, attribute: "filename")
175
+ assert_includes response.body, remove_file_document_path(@doc, attribute: "filename")
176
+ assert_includes response.body, "fi-pencil"
177
+ assert_includes response.body, "data-turbo-confirm"
178
+ end
179
+
180
+ test "remove and replace icons follow the permissions" do
181
+ with_file
182
+ Ability.restrictions = -> { cannot [ :remove_file, :replace_file ], Document }
183
+ get document_path(@doc, attribute: "filename", form_element: "simple_file_field", update: @frame),
184
+ headers: frame_headers(@frame)
185
+
186
+ refute_includes response.body, "remove_file"
187
+ refute_includes response.body, "fi-pencil"
188
+ assert_includes response.body, "download_file"
189
+ end
190
+
191
+ test "the record shows a trash panel frame and file events in versions" do
192
+ with_file
193
+ @doc.inline_forms_remove_file!(:filename, by: 1)
194
+ row = "document_#{@doc.id}"
195
+ get document_path(@doc, update: row), headers: frame_headers(row)
196
+
197
+ assert_response :success
198
+ assert_includes response.body, %(<turbo-frame id="document_#{@doc.id}_file_trash">)
199
+ assert_includes response.body, "Removed files (1)"
200
+ assert_includes response.body, "Versions (3)", "create version + upload event + removal event"
201
+ end
202
+
203
+ # ---- remove / restore / purge -----------------------------------------
204
+
205
+ test "remove moves the file to the trash and offers undo" do
206
+ with_file
207
+ post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
208
+
209
+ assert_response :success
210
+ @doc.reload
211
+ assert_nil @doc.filename
212
+ assert_nil @doc.data
213
+ entry = trash.sole
214
+ assert_equal "removed", entry.reason
215
+ assert_equal PLAN, entry.file_data
216
+ assert_includes response.body, "plan.pdf removed"
217
+ assert_includes response.body, "plan.pdf removed"
218
+ assert_equal 1, response.body.scan("trash_id=#{entry.id}").size, "one undo link: the notice's, not also the plus link's"
219
+ assert_includes response.body, %(target="document_#{@doc.id}_file_trash")
220
+ assert_includes response.body, %(target="document_#{@doc.id}_versions")
221
+ end
222
+
223
+ test "removing an empty slot twice creates no empty trash row" do
224
+ post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
225
+ assert_response :success
226
+ assert_empty trash
227
+ end
228
+
229
+ test "remove works on a record that fails unrelated validations" do
230
+ with_file
231
+ Document.where(id: @doc.id).update_all(title: nil)
232
+ post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
233
+
234
+ assert_response :success
235
+ assert_nil @doc.reload.data
236
+ end
237
+
238
+ test "remove needs :remove_file" do
239
+ with_file
240
+ Ability.restrictions = -> { cannot :remove_file, Document }
241
+ post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
242
+
243
+ assert_response :forbidden
244
+ assert_equal PLAN, @doc.reload.data
245
+ end
246
+
247
+ test "an undeclared attribute is refused" do
248
+ post remove_file_document_path(@doc, attribute: "title", update: @frame), headers: stream_headers(@frame)
249
+ assert_response :bad_request
250
+ end
251
+
252
+ test "restore puts the bytes back and empties the trash row" do
253
+ with_file
254
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
255
+ post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
256
+ headers: stream_headers(@frame)
257
+
258
+ assert_response :success
259
+ @doc.reload
260
+ assert_equal PLAN, @doc.data
261
+ assert_equal "plan.pdf", @doc.filename
262
+ entry.reload
263
+ assert entry.restored?
264
+ assert_nil entry.file_data
265
+ end
266
+
267
+ test "restore into an occupied slot swaps: the current file goes to the trash" do
268
+ with_file
269
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
270
+ with_file(OTHER, "other.pdf")
271
+ post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
272
+ headers: stream_headers(@frame)
273
+
274
+ assert_equal PLAN, @doc.reload.data
275
+ swapped = trash.where(reason: "replaced").sole
276
+ assert_equal OTHER, swapped.file_data
277
+ end
278
+
279
+ test "restore twice is a no-op the second time" do
280
+ with_file
281
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
282
+ 2.times do
283
+ post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
284
+ headers: stream_headers(@frame)
285
+ end
286
+
287
+ assert_includes response.body, "can no longer be restored"
288
+ assert_equal 1, trash.count
289
+ end
290
+
291
+ test "restore needs :restore_file, and :update for an empty slot" do
292
+ with_file
293
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
294
+ Ability.restrictions = -> { cannot :update, Document, [ :filename ] }
295
+ post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
296
+ headers: stream_headers(@frame)
297
+
298
+ assert_response :forbidden
299
+ assert_nil @doc.reload.data
300
+ end
301
+
302
+ test "a trash entry of another record is not found" do
303
+ other = Document.create!(title: "Dossier B", filename: upload(PLAN))
304
+ entry = other.inline_forms_remove_file!(:filename, by: 1)
305
+ post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
306
+ headers: stream_headers(@frame)
307
+
308
+ assert_response :not_found
309
+ assert entry.reload.trashed?
310
+ end
311
+
312
+ test "purge deletes the bytes and keeps the tombstone" do
313
+ with_file
314
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
315
+ post purge_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
316
+ headers: stream_headers(@frame)
317
+
318
+ assert_response :success
319
+ entry.reload
320
+ assert entry.purged?
321
+ assert_nil entry.file_data
322
+ assert_equal "plan.pdf", entry.filename, "no redaction by default"
323
+ assert_equal Digest::SHA256.hexdigest(PLAN), entry.checksum
324
+ end
325
+
326
+ test "purge with redaction blanks the filename" do
327
+ InlineForms.file_trash_redact_on_purge = true
328
+ with_file
329
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
330
+ assert entry.purge!(by: 1)
331
+
332
+ assert_nil entry.reload.filename
333
+ assert entry.redacted?
334
+ end
335
+
336
+ test "purge needs :purge_file" do
337
+ with_file
338
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
339
+ Ability.restrictions = -> { cannot :purge_file, Document }
340
+ post purge_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
341
+ headers: stream_headers(@frame)
342
+
343
+ assert_response :forbidden
344
+ assert entry.reload.trashed?
345
+ end
346
+
347
+ test "a purged entry can be neither restored nor purged again" do
348
+ with_file
349
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
350
+ assert entry.purge!(by: 1)
351
+
352
+ refute entry.purge!(by: 1)
353
+ assert_raises(InlineForms::TrashedFile::NotRestorable) { entry.restore!(by: 1) }
354
+ end
355
+
356
+ # ---- downloads --------------------------------------------------------
357
+
358
+ test "download_file sends the bytes as an attachment, never cached" do
359
+ with_file
360
+ get download_file_document_path(@doc, attribute: "filename")
361
+
362
+ assert_response :success
363
+ assert_equal PLAN, response.body.b
364
+ assert_match(/attachment/, response.headers["Content-Disposition"])
365
+ assert_equal "no-store", response.headers["Cache-Control"]
366
+ end
367
+
368
+ test "download_file of an empty slot is a 404, not an empty file" do
369
+ get download_file_document_path(@doc, attribute: "filename")
370
+ assert_response :not_found
371
+ end
372
+
373
+ test "download_file needs :read on the attribute" do
374
+ with_file
375
+ Ability.restrictions = -> { cannot :read, Document, [ :filename ] }
376
+ get download_file_document_path(@doc, attribute: "filename")
377
+ assert_response :forbidden
378
+ end
379
+
380
+ test "a trashed file downloads as octet-stream attachment until purged" do
381
+ with_file(PLAN, "page.html")
382
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
383
+ get trashed_file_document_path(@doc, attribute: "filename", trash_id: entry.id)
384
+
385
+ assert_response :success
386
+ assert_equal PLAN, response.body.b
387
+ assert_equal "application/octet-stream", response.media_type
388
+ assert_match(/attachment/, response.headers["Content-Disposition"])
389
+
390
+ entry.purge!(by: 1)
391
+ get trashed_file_document_path(@doc, attribute: "filename", trash_id: entry.id)
392
+ assert_response :not_found
393
+ end
394
+
395
+ test "downloading a trashed file needs :download_trashed_file" do
396
+ with_file
397
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
398
+ Ability.restrictions = -> { cannot :download_trashed_file, Document }
399
+ get trashed_file_document_path(@doc, attribute: "filename", trash_id: entry.id)
400
+ assert_response :forbidden
401
+ end
402
+
403
+ # ---- panel --------------------------------------------------------------
404
+
405
+ test "the open panel lists entries with actions; closed shows the count" do
406
+ with_file
407
+ @doc.inline_forms_remove_file!(:filename, by: 1)
408
+ panel = "document_#{@doc.id}_file_trash"
409
+ get file_trash_document_path(@doc, update: panel, slots: "filename,plan_filename"), headers: frame_headers(panel)
410
+
411
+ assert_response :success
412
+ assert_includes response.body, "plan.pdf"
413
+ assert_includes response.body, "delete permanently"
414
+ assert_includes response.body, "restorable until"
415
+ end
416
+
417
+ test "the panel needs :read_file_trash" do
418
+ Ability.restrictions = -> { cannot :read_file_trash, Document }
419
+ panel = "document_#{@doc.id}_file_trash"
420
+ get file_trash_document_path(@doc, update: panel), headers: frame_headers(panel)
421
+ assert_response :forbidden
422
+ end
423
+
424
+ # ---- destroy / revert --------------------------------------------------
425
+
426
+ test "destroying a record trashes its files; undoing the destroy brings them back" do
427
+ Document.require_filename = true
428
+ with_file
429
+ @doc.update!(plan_filename: upload(OTHER, "plan2.pdf"))
430
+ row = "document_#{@doc.id}"
431
+ delete document_path(@doc, update: row), headers: frame_headers(row)
432
+ assert_equal 2, trash.where(reason: "record_destroyed").count
433
+
434
+ version = PaperTrail::Version.where(item_type: "Document", item_id: @doc.id, event: "destroy").last
435
+ post revert_document_path(version.id, update: "documents_list"), headers: stream_headers("documents_list")
436
+
437
+ assert_response :success
438
+ restored = Document.find(@doc.id)
439
+ assert_equal PLAN, restored.data
440
+ assert_equal "plan.pdf", restored.filename
441
+ assert_equal OTHER, restored.plan_data
442
+ assert trash.where(reason: "record_destroyed").all?(&:restored?)
443
+ end
444
+
445
+ test "undoing a destroy after a purge brings the record back without that file" do
446
+ with_file
447
+ row = "document_#{@doc.id}"
448
+ delete document_path(@doc, update: row), headers: frame_headers(row)
449
+ trash.sole.purge!(by: 1)
450
+
451
+ version = PaperTrail::Version.where(item_type: "Document", item_id: @doc.id, event: "destroy").last
452
+ post revert_document_path(version.id, update: "documents_list"), headers: stream_headers("documents_list")
453
+
454
+ restored = Document.find(@doc.id)
455
+ assert_nil restored.filename
456
+ assert_nil restored.data
457
+ end
458
+
459
+ test "reverting an update never touches a slot, even from an old version carrying a filename" do
460
+ with_file
461
+ @doc.update!(title: "Renamed")
462
+ version = PaperTrail::Version.where(item_type: "Document", item_id: @doc.id, event: "update").last
463
+ # A pre-adoption version row still has the slot columns in it.
464
+ version.update_columns(object: version.object.sub("title: Dossier A", "title: Dossier A\nfilename: old.pdf"))
465
+
466
+ post revert_document_path(version.id, update: "document_#{@doc.id}"), headers: stream_headers("document_#{@doc.id}")
467
+
468
+ assert_response :success
469
+ @doc.reload
470
+ assert_equal "Dossier A", @doc.title
471
+ assert_equal "plan.pdf", @doc.filename
472
+ assert_equal PLAN, @doc.data
473
+ end
474
+
475
+ # ---- retention ----------------------------------------------------------
476
+
477
+ test "an expired entry is no longer restorable and the sweep purges it" do
478
+ with_file
479
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
480
+ entry.update_columns(purge_after: 1.minute.ago)
481
+
482
+ assert_raises(InlineForms::TrashedFile::NotRestorable) { entry.reload.restore!(by: 1) }
483
+ assert_equal 1, InlineForms::FileTrashSweep.run!
484
+ assert entry.reload.purged?
485
+ assert_nil entry.purged_by_id
486
+ refute InlineForms::FileTrashSweep.overdue?
487
+ end
488
+
489
+ test "retention nil never expires" do
490
+ InlineForms.file_trash_retention = nil
491
+ with_file
492
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
493
+ assert_nil entry.purge_after
494
+ assert_equal 0, InlineForms::TrashedFile.purge_expired!
495
+ end
496
+
497
+ test "the sweep is overdue when it never ran" do
498
+ assert InlineForms::FileTrashSweep.overdue?
499
+ end
500
+
501
+ # ---- global page ----------------------------------------------------------
502
+
503
+ test "the global page lists trashed files and bulk-purges selected ones" do
504
+ with_file
505
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
506
+ get inline_forms_file_trash_path
507
+
508
+ assert_response :success
509
+ assert_includes response.body, "plan.pdf"
510
+
511
+ post inline_forms_file_trash_purge_path, params: { ids: [ entry.id ] }
512
+ assert_response :redirect
513
+ assert entry.reload.purged?
514
+ end
515
+
516
+ test "the global page offers bulk purge only when a listed row is purgeable" do
517
+ with_file
518
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
519
+ get inline_forms_file_trash_path(state: "trashed")
520
+ assert_includes response.body, %(id="trash_entry_#{entry.id}")
521
+ assert_includes response.body, I18n.t("inline_forms.files.global.purge_selected")
522
+
523
+ entry.restore!(by: 1)
524
+ get inline_forms_file_trash_path(state: "restored")
525
+ assert_includes response.body, "plan.pdf"
526
+ refute_includes response.body, %(name="ids[]")
527
+ refute_includes response.body, I18n.t("inline_forms.files.global.purge_selected")
528
+ end
529
+
530
+ test "the global page restores and downloads" do
531
+ with_file
532
+ entry = @doc.inline_forms_remove_file!(:filename, by: 1)
533
+ get inline_forms_file_trash_download_path(entry)
534
+ assert_equal PLAN, response.body.b
535
+
536
+ post inline_forms_file_trash_restore_path(entry)
537
+ assert_response :redirect
538
+ assert_equal PLAN, @doc.reload.data
539
+ end
540
+
541
+ test "the global page purges files of deleted records" do
542
+ with_file
543
+ row = "document_#{@doc.id}"
544
+ delete document_path(@doc, update: row), headers: frame_headers(row)
545
+ entry = trash.sole
546
+
547
+ post inline_forms_file_trash_purge_path, params: { ids: [ entry.id ] }
548
+ assert entry.reload.purged?
549
+ end
550
+
551
+ test "the global page needs :read_file_trash on :all" do
552
+ Ability.restrictions = lambda do
553
+ cannot :manage, :all
554
+ can :read, :all
555
+ can :read_file_trash, Document
556
+ end
557
+ get inline_forms_file_trash_path
558
+ assert_response :forbidden
559
+ end
560
+
561
+ test "a bulk purge is all or nothing when one row is not allowed" do
562
+ with_file
563
+ mine = @doc.inline_forms_remove_file!(:filename, by: 1)
564
+ @doc.update!(plan_filename: upload(OTHER, "x.pdf"))
565
+ theirs = @doc.inline_forms_remove_file!(:plan_filename, by: 1)
566
+ Ability.restrictions = -> { cannot :purge_file, Document, [ :plan_filename ] }
567
+
568
+ post inline_forms_file_trash_purge_path, params: { ids: [ mine.id, theirs.id ] }
569
+
570
+ assert_response :forbidden
571
+ assert mine.reload.trashed?
572
+ assert theirs.reload.trashed?
573
+ end
574
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration_test_helper"
4
+
5
+ # layouts/inline_forms <title>: t('application_name') used to render the
6
+ # html_safe <span class="translation_missing"> into <title> when the host had
7
+ # no translation (every page of a fresh example app). It now falls back to
8
+ # ApplicationHelper#application_name ("Dummy" here) and is always plain text.
9
+ class LayoutTitleTest < InlineFormsIntegrationTestCase
10
+ teardown do
11
+ I18n.backend.reload!
12
+ end
13
+
14
+ def page_title
15
+ response.body[%r{<title>(.*?)</title>}m, 1]
16
+ end
17
+
18
+ test "title falls back to the application_name helper without a translation" do
19
+ get widgets_path
20
+
21
+ assert_response :success
22
+ assert_equal "Dummy v#{InlineForms::VERSION}", page_title
23
+ refute_includes page_title, "translation_missing"
24
+ end
25
+
26
+ test "title uses the application_name translation when the host defines one" do
27
+ I18n.backend.store_translations(:en, application_name: "Widget Works")
28
+
29
+ get widgets_path
30
+
31
+ assert_response :success
32
+ assert_equal "Widget Works v#{InlineForms::VERSION}", page_title
33
+ end
34
+
35
+ test "title in nl does not show the gem's old placeholder" do
36
+ I18n.with_locale(:nl) do
37
+ original = I18n.default_locale
38
+ I18n.default_locale = :nl
39
+ get widgets_path
40
+ ensure
41
+ I18n.default_locale = original
42
+ end
43
+
44
+ assert_response :success
45
+ assert_equal "Dummy v#{InlineForms::VERSION}", page_title
46
+ end
47
+
48
+ test "markup in the translation is escaped, never rendered into the title" do
49
+ I18n.backend.store_translations(:en, application_name: "<b>Bold</b>")
50
+
51
+ get widgets_path
52
+
53
+ assert_response :success
54
+ assert_equal "&lt;b&gt;Bold&lt;/b&gt; v#{InlineForms::VERSION}", page_title
55
+ end
56
+ end
@@ -44,6 +44,15 @@ class ModelTopBarTest < InlineFormsIntegrationTestCase
44
44
  refute_includes response.body, "Beta"
45
45
  end
46
46
 
47
+ test "no schema GUI link in the engine top bar, even with the schema routes drawn" do
48
+ assert respond_to?(:inline_forms_schema_new_path), "precondition: the dummy draws the schema routes"
49
+ get widgets_path
50
+
51
+ assert_response :success
52
+ refute_includes response.body, "+ field"
53
+ refute_includes response.body, %(href="#{inline_forms_schema_new_path}")
54
+ end
55
+
47
56
  test "non-model page with no @Klass renders title only, no new-record link" do
48
57
  get stats_path
49
58